---
title: "Split Frontend and Auth Workers"
url: "https://effect-auth.itsbroly.com/recipes/split-frontend-auth-workers/"
description: "Put an auth Worker behind a same-origin frontend route with a Cloudflare Service Binding."
---



Start with one Worker. Split auth only when independent deployments, ownership, resource isolation, or reuse by several Workers outweigh another deployment boundary. Cloudflare [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) make the split inexpensive, but they do not remove operational coupling.

When separation is justified, keep auth same-origin from the browser's perspective:

```text
                         public origin                    private hop
┌─────────┐  /auth/*  ┌─────────────────┐  AUTH_BACKEND  ┌─────────────┐
│ browser │ ─────────>│ frontend Worker │ ──────────────>│ auth Worker │
└─────────┘ <─────────└─────────────────┘ <──────────────└─────────────┘
              Set-Cookie / JSON             Service Binding
```

The frontend owns the public hostname and a [TanStack Start server route](https://tanstack.com/start/latest/docs/framework/react/guide/server-routes). The auth Worker can remain unreachable from the public Internet. They are separate Worker resources and deployments; deploy the auth target before a caller that first binds it, and make backend changes compatible with the currently deployed frontend.

## File map [#file-map]

```text
alchemy.run.ts                    resources, bindings, deployment graph
src/routes/auth/$.ts              same-origin transparent proxy
src/workers/auth-backend.ts       auth Worker fetch entrypoint
src/server/auth.ts                effect-auth layers and HTTP handler
src/server/env.ts                 lazy typed cloudflare:workers env access
```

See the maintained [`tanstack-cloudflare-auth-split`](https://github.com/nr1brolyfan/effect-auth/tree/main/examples/tanstack-cloudflare-auth-split) example for the complete application.

## Declare two Workers [#declare-two-workers]

In Alchemy v2, resources assigned to another Worker's environment become bindings. This compact version follows the example's Effect API; consult Alchemy's current [bindings](https://alchemy.run/concepts/bindings) and [Cloudflare Worker guide](https://alchemy.run/guides/cloudflare-worker) when upgrading Alchemy.

```ts
import * as Cloudflare from "alchemy/Cloudflare";

const Database = Cloudflare.D1.Database("AuthDatabase", {
  migrationsDir: "./migrations",
});

const AuthBackend = Cloudflare.Worker("AuthBackend", {
  main: "./src/workers/auth-backend.ts",
  env: {
    DB: Database,
    AUTH_SECRET: process.env.AUTH_SECRET!,
    AUTH_PUBLIC_URL: process.env.AUTH_PUBLIC_URL!,
    AUTH_ALLOWED_ORIGINS: process.env.AUTH_ALLOWED_ORIGINS!,
  },
});

const Website = Cloudflare.Website.Vite("Website", {
  env: { AUTH_BACKEND: AuthBackend },
});
```

Bind databases, Durable Objects, email, and auth secrets only to `AuthBackend` unless the frontend genuinely needs them. Treat secrets with the mechanism required by your pinned Alchemy version; do not commit them. Type the frontend binding as `AUTH_BACKEND: Service` (or infer its environment from the resource declaration).

## Forward without rebuilding [#forward-without-rebuilding]

```ts
import { createFileRoute } from "@tanstack/react-router";
import { env } from "../../server/env";

export const Route = createFileRoute("/auth/$")({
  server: {
    handlers: {
      GET: ({ request }) => env.AUTH_BACKEND.fetch(request),
      POST: ({ request }) => env.AUTH_BACKEND.fetch(request),
    },
  },
});
```

Pass the original `Request` and return the original `Response`. Do not parse/re-encode bodies, copy headers into plain objects, rewrite the URL, or manufacture a response. This preserves streaming, `Cookie`, `Origin`, Cloudflare metadata and every `Set-Cookie` header. The browser therefore calls `/auth/*` normally, and [`createAuthClient()`](/clients/browser-client/) needs neither a cross-origin base URL nor CORS configuration.

The auth backend sees the public frontend URL because the original request is forwarded. Configure `AUTH_PUBLIC_URL` and `AUTH_ALLOWED_ORIGINS` to that exact browser origin, including scheme and non-default port. If request metadata enables `trustProxyHeaders`, do so only because requests enter through your controlled Cloudflare binding; never expose the backend directly while trusting client-supplied forwarding headers. Validate which Cloudflare headers your metadata policy consumes, and test the resulting client IP and origin.

## Develop, test, and ship [#develop-test-and-ship]

Alchemy's `alchemy dev` runs the resource graph and local bindings; the example uses `bun run dev:local`. Raw `vite dev` does not supply Cloudflare bindings. Wrangler can also run both Workers, although its multi-config mode is documented as experimental.

Test the public frontend URL, not the backend: sign in, read the session, refresh/logout, and assert multiple `Set-Cookie` values survive. Cover missing binding/backend failures and unsafe requests with missing, allowed, and hostile `Origin`. Production should use HTTPS, secure cookies, real secrets and email, exact WebAuthn/public-origin configuration, migrations before traffic, and staged backend-then-frontend rollouts. Service calls count as Worker invocations/subrequests, so monitor limits and failures.

A direct cross-origin auth hostname instead adds CORS, credentialed fetches, cookie `SameSite`/domain constraints, preflights, and a larger exposed surface. Prefer this same-origin route, or keep auth in the frontend Worker until a concrete reason makes the split worthwhile.

