1---
2title: Protect a Worker with Access
3description: Put Cloudflare Access in front of a Worker with the access prop — per-Worker policies, shared applications, ctx.access identity at runtime, and a local dev simulation.
4sidebar:
5 order: 20
6---
7
8Cloudflare Access puts a login wall in front of your Worker — covering
9its custom domains, routes, `workers.dev` URL, and version preview
10URLs — and hands your code the authenticated identity at runtime. In
11alchemy this is a property of the Worker: set the `access` prop.
12
13The prop has two forms — a **dedicated application** owned by the
14Worker (`access: { policies }`) for per-Worker rules, and a **shared
15application** (`access: App`) when several Workers sit behind one
16policy set. Each has its own section below.
17
18## Dedicated application (per-Worker policies)
19
20The simplest form declares the policies right on the Worker. Alchemy
21creates a dedicated Access application for it (Cloudflare attaches
22policies to *applications*, so per-Worker policies mean a per-Worker
23application — created, updated, and deleted with the Worker, and
24namespaced under it as `Api/Access`):
25
26```typescript
27import * as Cloudflare from "alchemy/Cloudflare";
28import * as Effect from "effect/Effect";
29import * as HttpServerResponse from "effect/http/HttpServerResponse";
30
31export default Cloudflare.Worker(
32 "Api",
33 {
34 main: import.meta.url,
35 access: {
36 policies: [
37 { decision: "allow", include: [{ emailDomain: "example.com" }] },
38 ],
39 },
40 },
41 Effect.gen(function* () {
42 return {
43 fetch: Effect.gen(function* () {
44 return HttpServerResponse.text("team only");
45 }),
46 };
47 }),
48);
49```
50
51Unauthenticated requests are redirected to your team's Access login
52page before they ever reach the Worker. Anyone with a verified
53`@example.com` email gets through.
54
55## Read who's calling
56
57Once a request passes Access, the identity is available from
58`ctx.access` — one yield away via `Cloudflare.Access.Context`:
59
60```typescript
61fetch: Effect.gen(function* () {
62 const access = yield* Cloudflare.Access.Context;
63 if (access === undefined) {
64 return HttpServerResponse.text("Access required", { status: 403 });
65 }
66 const identity = yield* access.getIdentity();
67 return yield* HttpServerResponse.json({
68 aud: access.aud,
69 email: identity?.email,
70 groups: identity?.groups,
71 });
72}),
73```
74
75`access` is `undefined` when the request did not come through
76Cloudflare Access (for example a route you deliberately left open).
77`getIdentity()` resolves the user's email, name, groups, identity
78provider, device posture, and service-token fields.
79
80## Test locally
81
82In production, `ctx.access` is populated by Cloudflare's edge after a
83request passes the Access login wall. Under `alchemy dev` your Worker
84runs in local workerd — there is no edge and no login — so `ctx.access`
85would always be `undefined` and the authenticated branch of your
86handler could never run locally.
87
88`dev.access` stubs that state: when set, every local request behaves
89as if this one user had logged in — `ctx.access` carries the given
90`aud` and `getIdentity()` resolves the given identity:
91
92```typescript
93export default Cloudflare.Worker(
94 "Api",
95 {
96 main: import.meta.url,
97 access: {
98 policies: [
99 { decision: "allow", include: [{ emailDomain: "example.com" }] },
100 ],
101 },
102 dev: {
103 access: {
104 aud: "dev",
105 identity: { email: "dev@example.com" },
106 },
107 },
108 },
109 /* ... */
110);
111```
112
113Remove the `dev.access` block to simulate unauthenticated requests
114(`Cloudflare.Access.Context` yields `undefined`), so both branches of
115your handler are exercisable without deploying. Changing the block
116restarts the dev server with the new identity. It is inert on deploy:
117the deployed Worker always gets the real edge-populated `ctx.access`,
118never the stub.
119
120## Writing policy rules
121
122Rules use Cloudflare's own shapes, with a scalar shorthand for the
123single-parameter kinds — both spellings are equivalent:
124
125```typescript
126policies: [
127 {
128 decision: "allow",
129 include: [
130 { emailDomain: "example.com" }, // { emailDomain: { domain: "example.com" } }
131 { email: "sam@partner.com" }, // { email: { email: "sam@partner.com" } }
132 ],
133 exclude: [{ email: "intern@example.com" }],
134 require: [{ geo: "US" }], // { geo: { countryCode: "US" } }
135 sessionDuration: "12h",
136 },
137]
138```
139
140`include` rules combine with OR, `require` with AND, `exclude` with
141NOT. Parameter-less kinds collapse to bare strings (`"everyone"`,
142`"certificate"`, `"anyValidServiceToken"`); multi-parameter kinds
143(`gsuite`, `okta`, `saml`, `oidc`, `azureAD`, `githubOrganization`,
144...) keep Cloudflare's wire shape, so anything in the
145[Access policy docs](https://developers.cloudflare.com/cloudflare-one/policies/access/)
146pastes in directly.
147
148## Shared application (one policy set, many Workers)
149
150When several Workers should sit behind the same policy set, declare
151the application once and pass it to each Worker directly:
152
153```typescript
154import * as Cloudflare from "alchemy/Cloudflare";
155
156export const TeamOnly = Cloudflare.Access.Application("TeamOnly", {
157 type: "self_hosted",
158 policies: [
159 { decision: "allow", include: [{ emailDomain: "example.com" }] },
160 ],
161});
162```
163
164```typescript
165import { TeamOnly } from "./stack.ts";
166
167export default Cloudflare.Worker(
168 "Api",
169 {
170 main: import.meta.url,
171 access: TeamOnly,
172 },
173 /* ... */
174);
175```
176
177Each enrolled Worker contributes its destinations to the application
178through the engine's binding contract, so the application always
179converges on exactly the set of currently-enrolled Workers — removing
180the `access` prop (or the Worker itself) un-enrolls it on the next
181deploy. Keep in mind Access policies are **application-wide**: every
182Worker enrolled in `TeamOnly` is gated by the same policy set. For
183per-Worker rules, use the dedicated `{ policies }` form above.
184
185On the dedicated form, set `previews: false` to protect production
186traffic only, leaving version preview URLs open.
187
188## Protect every Worker on the account
189
190An account-level application covers all current **and future**
191Workers. Hostname policies beat Worker policies beat account policies,
192so an individual Worker can still be opened up (or locked down
193tighter) with its own application:
194
195```typescript
196yield* Cloudflare.Access.Application("ProtectAllWorkers", {
197 type: "self_hosted",
198 destinations: [
199 Cloudflare.Access.AllWorkers,
200 Cloudflare.Access.AllWorkerPreviews,
201 ],
202 policies: [
203 { decision: "allow", include: [{ emailDomain: "example.com" }] },
204 ],
205});
206```
207
208## Websites too
209
210`access` is part of the shared Worker props, so every
211`Cloudflare.Website.*` framework accepts it unchanged:
212
213```typescript
214const app = yield* Cloudflare.Website.Astro("Docs", {
215 access: {
216 policies: [
217 { decision: "allow", include: [{ emailDomain: "example.com" }] },
218 ],
219 },
220});
221```
222
223## See also
224
225- [Application API reference](/providers/cloudflare/access/application)
226- [Policy API reference](/providers/cloudflare/access/policy)
227- [Worker API reference](/providers/cloudflare/workers/worker)
228- [Secrets & env](/cloudflare/security/secrets-env) — for service-to-service
229 auth without a human login, pair Access service tokens with policies
230 using `serviceToken` / `"anyValidServiceToken"` rules.