# Authentication for Serverless and Edge Deployments - Part 2

> Part 2 of 3. Start with [Authentication for Serverless and Edge Deployments](https://clerk.com/articles/authentication-for-serverless-and-edge-deployments.md).

Welcome to Part 2 of our series on _Authentication for Serverless and Edge Deployments_. In Part 1, we explored the fundamentals of serverless architecture, the core challenges of distributed authentication, and standard strategies like stateless JWTs.

In this installment, we dive into platform-specific constraints and considerations—covering Vercel, Cloudflare, AWS, and more. We will also examine the complexities of managing authentication across a monorepo, where multiple apps, platforms, and services share a unified user base. Finally, we provide a comprehensive, end-to-end guide to implementing seamless, low-latency authentication at the edge using Clerk Core 3.

## Platform-Specific Considerations

### Vercel (Next.js 16 + Vercel Functions)

**Next.js 16 `proxy.ts` runs exclusively on Node.js.** The runtime is not configurable and `proxy.ts` does not support the Route Segment Config `runtime` option — setting `export const runtime = 'edge'` in `proxy.ts` throws a build-time error ([Next.js `proxy.ts` reference](https://nextjs.org/docs/app/api-reference/file-conventions/proxy), [Next.js 16 release blog](https://nextjs.org/blog/next-16)).

**Vercel Edge Runtime is still documented and functional but Vercel recommends migrating new work to Node.js.** The current Vercel Edge Runtime docs display an explicit warning recommending migration to Node.js for improved performance and reliability, and note that both runtimes now run on Fluid Compute with identical Active CPU pricing ([Vercel Edge Runtime](https://vercel.com/docs/functions/runtimes/edge), [Vercel Edge Functions deprecated](https://vercel.com/docs/functions/runtimes/edge/edge-functions)).

**Vercel Routing Middleware** (platform-layer, distinct from `proxy.ts`) still uses the Edge runtime and is not deprecated ([Vercel Routing Middleware docs](https://vercel.com/docs/routing-middleware), [Vercel changelog unification](https://vercel.com/changelog/edge-middleware-and-edge-functions-are-now-powered-by-vercel-functions)).

**Fluid Compute** (many-to-one instance sharing) and Active CPU pricing are the architectural reason Vercel consolidated Edge Functions and Node.js Functions onto one runtime + pricing model ([Introducing Fluid Compute](https://vercel.com/blog/introducing-fluid-compute), [How Fluid Compute Works](https://vercel.com/blog/how-fluid-compute-works-on-vercel), [Vercel Active CPU pricing](https://vercel.com/blog/introducing-active-cpu-pricing-for-fluid-compute)).

Node runtime bundle limit is 250MB; memory up to 4GB; max duration 300–800s ([Vercel Functions limitations](https://vercel.com/docs/functions/limitations)).

> For Next.js 16 route handlers, omit the `runtime` config or explicitly set `'nodejs'`. Opting into the Edge runtime offers no benefit alongside a Node.js-only `proxy.ts`.

### Cloudflare Workers (primary V8-isolate example)

Cloudflare Workers run in V8 isolates with Web Crypto. CPU-time limits are Free 10ms, Paid 5 min ([Cloudflare Workers platform limits](https://developers.cloudflare.com/workers/platform/limits/)).

- **Worker Sharding** — Cloudflare reports approximately 90% reduction in evictions with sharding; Workers start in under 5ms ([ByteByteGo — How Cloudflare eliminates cold starts](https://blog.bytebytego.com/p/how-cloudflare-eliminates-cold-starts)).
- **KV for shared JWKS/user cache** — hot key reads `<1ms`; p99 `<5ms` (since Oct 2025 update) ([Cloudflare KV performance](https://blog.cloudflare.com/faster-workers-kv/)).
- **Durable Objects** — single-instance pinning for rate limiting or strongly consistent session state ([Durable Objects](https://developers.cloudflare.com/durable-objects/)).
- **`nodejs_compat` flag** — compat date 2024-09-23+ enables Node-style `crypto`, `Buffer`, `fs`, HTTP, and Streams. While some Node-first libraries fail due to missing TCP/child processes, basic crypto gaps are closed ([Cloudflare Workers Node.js compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)).
- **[Hono](https://hono.dev/docs/getting-started/cloudflare-workers)** is a common Workers framework with an official [Clerk middleware](https://github.com/honojs/middleware/tree/main/packages/clerk-auth) package (`@hono/clerk-auth`).

### AWS Lambda and Lambda\@Edge

- Cold starts: Node.js 22 arm64 p50 \~294ms optimized; p99 in the 500–700ms range ([Node.js 22 Lambda benchmarks](https://speedrun.nobackspacecrew.com/blog/2025/07/21/the-fastest-node-22-lambda-coldstart-configuration.html), [edgedelta AWS Lambda cold start costs](https://edgedelta.com/company/knowledge-center/aws-lambda-cold-start-cost)).
- **Provisioned Concurrency** eliminates cold starts but adds fixed cost.
- **Lambda\@Edge restrictions**: must deploy in us-east-1; no ARM64; no layers; no VPC; viewer request truncated at 40KB; CloudFront Functions cannot do RS256 signature verification ([AWS Lambda@Edge restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-edge-function-restrictions.html)).
- **API Gateway JWT authorizer** (native, no Lambda invocation) vs. **Lambda authorizer** (REQUEST- or TOKEN-based, cacheable, custom logic) ([API Gateway JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html), [Lambda authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)).
- **SnapStart** was expanded to Python and .NET at re:Invent 2025; Node.js is not yet supported ([AWS Lambda SnapStart docs](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html)).

### Netlify Edge Functions

- Deno-based; 20MB compressed bundle; 512MB memory; 50ms CPU per request; 40s response header timeout ([Netlify Edge Functions overview](https://docs.netlify.com/build/edge-functions/overview/), [Netlify Edge limits](https://docs.netlify.com/build/edge-functions/limits/)).
- Web Crypto and `node:` prefix modules are supported.
- No cross-subdomain cookies on `.netlify.app` domains.
- Netlify lacks an official Clerk Edge integration; the documented path is SPA/React client-side only ([Netlify Clerk integration page](https://www.netlify.com/integrations/clerk/)).

### Deno Deploy and Bun

- **Deno Deploy**: 35+ global PoPs; \~3ms cold start; 10ms p50; 38ms p99; Deno KV for distributed state; can also act as an OIDC provider ([Deno Deploy](https://deno.com/deploy), [Deno Deploy as OIDC Provider](https://docs.deno.com/deploy/reference/oidc/)).
- **Deno + JWT via Web Crypto**: `djwt` native library or `jose` npm; both use `SubtleCrypto` for RS256 ([Deno JWT with Web Crypto](https://docs.deno.com/examples/creating_and_verifying_jwt/)).
- **Bun**: not an edge runtime. Public beta as a Vercel Node runtime option ([Vercel Bun runtime docs](https://vercel.com/docs/functions/runtimes/bun)); Vercel reports approximately 28% latency reduction for CPU-bound Next.js rendering vs. Node.js ([Vercel Bun public beta](https://vercel.com/changelog/bun-runtime-now-in-public-beta-for-vercel-functions)).

### Other platforms (brief)

The platforms above cover the article's keyword cluster. Fastly Compute (WASI), Azure Static Web Apps, and Google Cloud Run exist but sit outside this article's scope. If the same "verify a signed token close to the request, re-verify close to the data" principle applies, the architectural patterns from Part 1 are portable; check each vendor's docs for runtime-specific constraints.

## Authentication in a Monorepo

### Why monorepos complicate auth

- Multiple apps (web, mobile, admin, background workers) share users but require different flows.
- Shared config drift: apps may independently install different auth SDK versions.
- Different runtime targets (Node.js, V8 isolates, React Native) coexist.

### Shared auth configuration strategies

- **Centralize environment variables.** Turborepo's `globalEnv` ensures that auth env vars are part of the cache key and do not leak between tasks that expect different secrets ([Turborepo environment variables](https://turborepo.com/docs/crafting-your-repository/using-environment-variables), [Turborepo configuration reference](https://turborepo.com/docs/reference/configuration)).
- **Centralize type definitions and utility functions** in a shared internal package (e.g., `packages/auth-config`). This houses `AppUser`, `AppSession`, and helpers wrapping common claim reads.
- **Do not wrap the auth SDK itself.** Each app installs its runtime-appropriate SDK (`@clerk/nextjs`, `@clerk/expo`, `@clerk/backend`). Shared SDK wrappers often collapse runtime differences or force lowest-common-denominator features.
- **Version and release the shared package** like any other internal library. Consumers update independently.
- **Pin SDK versions once** via [pnpm Catalogs](https://pnpm.io/catalogs) so every app stays in lockstep on core auth dependencies.

References: [Clerk T3 Turbo blog post](https://clerk.com/blog/clerk-t3-turbo.md), [Clerk T3 Turbo GitHub](https://github.com/clerk/t3-turbo-and-clerk), [Convex + Turborepo + Clerk monorepo](https://github.com/get-convex/turbo-expo-nextjs-clerk-convex-monorepo).

### Different auth approaches per service

#### Web application

- Cookie sessions + short-lived JWTs for edge middleware.
- UI components for sign-in, sign-up, and account management.
- Root provider setup (`ClerkProvider`, etc.).

#### Mobile and native API

- [Bearer tokens](https://clerk.com/glossary/bearer-token.md), refresh tokens, device-bound flows.
- [Expo](https://clerk.com/glossary/expo.md) / [React Native](https://clerk.com/glossary/react-native.md): secure token storage via `expo-secure-store` or platform equivalents.
- Native iOS (Swift) and Android (Kotlin) SDKs for production mobile apps.

#### Internal service-to-service

- Machine-to-machine tokens (client credentials or vendor-specific).
- Short-lived, scoped, and auditable.
- JWT format for edge workloads (free verification, networkless); opaque format for revocation-sensitive workloads.

#### Edge functions and middleware

- Networkless verification of short-lived JWTs (via environment PEM public key).
- Fallback to JWKS verification for out-of-band or ad-hoc calls.

### Microservices and auth boundaries

Auth boundaries (gateway vs. per-service) depend on whether downstream services trust gateway-signed identities.

**Authentication vs. authorization (explicit boundary).** Edge/gateway verification answers "is this a valid token for user U?" It does **not** answer "can U perform action A on resource R?" [Authorization](https://clerk.com/glossary/authorization.md) always runs closer to the data, after authentication is complete. Treating the gateway's "checked" flag as permission is how a gateway-bypass bug turns into an authorization hole.

**Safe identity propagation across services.**

- **Forwarding the original JWT** ensures claims remain verifiable by downstream services ([Microservices access-token pattern](https://microservices.io/patterns/security/access-token.html), [Building Microservices 2nd Edition](https://samnewman.io/books/building_microservices_2nd_edition/)).
- **Structured headers** (`x-user-id`, `x-org-id`) are cheaper but require trusted gateway boundaries. Strip inbound headers at the edge to prevent spoofing.
- **OpenTelemetry baggage** is good for non-sensitive correlation (tenant ID, request ID) across service hops without embedding PII in trace attributes ([OpenTelemetry context propagation](https://opentelemetry.io/docs/concepts/context-propagation/), [OpenTelemetry baggage](https://opentelemetry.io/docs/concepts/signals/baggage/)).
- **Service-mesh variants** — Istio `RequestAuthentication`, Linkerd mTLS, Consul Connect — push JWT validation and workload identity into the data plane ([Istio RequestAuthentication](https://istio.io/latest/docs/reference/config/security/request_authentication/), [Linkerd automatic mTLS](https://linkerd.io/2-edge/features/automatic-mtls/), [Consul Connect data plane](https://developer.hashicorp.com/consul/docs/architecture/data-plane/connect)).
- **Workload identity**, not user identity, is covered by SPIFFE/SPIRE SVIDs and complements JWT user identity in zero-trust meshes ([SPIFFE/SPIRE overview](https://spiffe.io/docs/latest/spiffe-about/overview/)).

Choosing centralized authorization engines (Oso, OPA, Cerbos) vs. per-service logic is a separate decision. See [Oso microservices authorization patterns](https://www.osohq.com/post/microservices-authorization-patterns) and [Contentstack monolith → microservices auth](https://www.contentstack.com/blog/tech-talk/from-legacy-systems-to-microservices-transforming-auth-architecture) for worked examples.

### Monorepo pitfalls to avoid

- Mixing session formats across apps (e.g., custom web sessions vs. Clerk on mobile).
- Inconsistent cookie names, domains, or security flags.
- Drifting JWT audiences or issuers, causing verifiers to reject valid tokens.
- Duplicating SDK installations with different versions across apps.

## Implementing Authentication with Clerk

This walkthrough assumes Clerk Core 3 (March 2026) and Next.js 16, with Cloudflare Workers as the canonical V8-isolate edge target ([Clerk Core 3 release](https://clerk.com/changelog/2026-03-03-core-3.md)).

### Why Clerk fits serverless and edge

- **Edge-compatible SDKs with unified configuration**. `@clerk/backend` is the canonical SDK for Node.js ≥20.9.0 and V8 isolates (Cloudflare Workers, Vercel Edge) ([Clerk Backend SDK overview](https://clerk.com/docs/reference/backend/overview.md), [Clerk Backend-Only SDK guide](https://clerk.com/docs/guides/development/sdk-development/backend-only.md)). One `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` pair serves every runtime (`@clerk/nextjs`, `@clerk/backend`, `@clerk/expo`) without separate edge/node configs.
- **Networkless JWT verification**. Configure `jwtKey` with Clerk's PEM public key for zero-network-round-trip verification. ([Clerk `verifyToken()` reference](https://clerk.com/docs/reference/backend/verify-token.md), [Clerk manual JWT verification](https://clerk.com/docs/guides/sessions/manual-jwt-verification.md)).
- **Managed JWKS and automatic key rotation**. Consumers never manage rotation schedules.
- **Built-in UI, organizations, MFA, passkeys, and device management**. Building these yourself requires months of engineering.
- **Typical verification latency**. A community benchmark places warm `jose`-style JWT verify at \~1.8ms on Cloudflare Workers and \~2.3ms on Vercel Edge, versus \~30ms on a traditional Node.js backend ([SSOJet community benchmark](https://ssojet.com/blog/how-to-validate-jwts-efficiently-at-the-edge-with-cloudflare-workers-and-vercel)). These community-sourced numbers illustrate order-of-magnitude improvements. Clerk's authoritative backbone remains networkless `jwtKey` verification and a 60-second session TTL.

### Installing and configuring Clerk (Next.js 16)

Required environment variables:

- `CLERK_PUBLISHABLE_KEY` — the [publishable key](https://clerk.com/glossary/publishable-key.md) from the Clerk dashboard.
- `CLERK_SECRET_KEY` — the [secret key](https://clerk.com/glossary/secret-key.md) used for server-to-server calls.
- `CLERK_JWT_KEY` — the PEM public key for networkless session JWT verification (copy from Dashboard → API Keys → PEM Public Key).

Installing:

```bash
pnpm add @clerk/nextjs
```

This article targets Next.js 16, which is where `proxy.ts` is available. `@clerk/nextjs` Core 3 peer dependencies require Next.js 16.0.10+ (or Next.js 15.2.8+ for apps still on the 15.x line using legacy `middleware.ts`) ([@clerk/nextjs on npm](https://www.npmjs.com/package/@clerk/nextjs), [Clerk Core 3 release](https://clerk.com/changelog/2026-03-03-core-3.md)).

Wrap the app root with `ClerkProvider`:

```tsx
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}
```

`ClerkProvider` automatically reads `CLERK_PUBLISHABLE_KEY` (or `NEXT_PUBLIC_...`).

### Edge middleware with Clerk and Next.js 16

The entry point is `clerkMiddleware()` inside `proxy.ts` ([Clerk `clerkMiddleware()` reference](https://clerk.com/docs/reference/nextjs/clerk-middleware.md)).

```ts
// proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/(.*)'])

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect()
  }
})

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
}
```

Reading the authenticated user in a Server Component — prefer `auth()` alone when session claims are enough:

```tsx
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server'

export default async function DashboardPage() {
  const { userId, sessionClaims } = await auth()
  if (!userId) return null

  const firstName = (sessionClaims?.firstName as string) ?? 'there'
  return <h1>Hello, {firstName}.</h1>
}
```

`auth()` reads the signed session claims from the request — no network round-trip. `currentUser()` fetches the full user record from Clerk's Backend API and counts toward the Backend API rate limit (1,000 requests per 10 seconds in production, 100 per 10 seconds in development) ([Clerk `currentUser()` reference](https://clerk.com/docs/references/nextjs/current-user.md), [Clerk Backend API rate limits](https://clerk.com/docs/guides/how-clerk-works/system-limits.md)). In high-volume edge settings, reserve `currentUser()` for necessary profile fields (`emailAddresses`, `publicMetadata`) and prefer `useUser()` on the client for display.

```tsx
// app/account/page.tsx — currentUser() is warranted here
import { auth, currentUser } from '@clerk/nextjs/server'

export default async function AccountPage() {
  const { userId } = await auth()
  if (!userId) return null

  const user = await currentUser()
  return (
    <section>
      <h1>{user?.firstName}</h1>
      <p>{user?.emailAddresses[0]?.emailAddress}</p>
    </section>
  )
}
```

`auth()` returns the session claims synchronously-awaited from the request; `currentUser()` loads the full user profile ([Clerk `auth()` reference](https://clerk.com/docs/references/nextjs/auth.md)). Add custom session claims via JWT templates so `auth()` carries commonly-read fields without Backend API calls.

> `proxy.ts` must not be the sole auth gate. CVE-2025-29927 allowed self-hosted Next.js deployments to skip `middleware.ts` entirely via a forwarded header ([NVD CVE-2025-29927](https://nvd.nist.gov/vuln/detail/CVE-2025-29927)). Always re-verify in Server Components, Route Handlers, or Server Actions — never treat middleware as the only check.

> In Next.js 16, `proxy.ts` runs exclusively on the Node.js runtime. The `runtime` Route Segment Config option is not supported in `proxy.ts` and setting it throws a build-time error. This is the "architectural edge" pattern (Node.js runtime sitting in front of origin). For true V8-isolate edge auth, see the "Clerk on Cloudflare Workers" section below.

### Clerk in Next.js 16 Route Handlers and Server Components

Next.js 16 Route Handlers default to Node.js. `auth()` functions identically to Server Components.

```ts
// app/api/profile/route.ts
import { auth, currentUser } from '@clerk/nextjs/server'

export async function GET() {
  const { userId } = await auth()
  if (!userId) {
    return Response.json({ error: 'unauthorized' }, { status: 401 })
  }

  const user = await currentUser()
  return Response.json({
    id: user?.id,
    email: user?.emailAddresses[0]?.emailAddress,
    firstName: user?.firstName,
  })
}
```

For handlers where you want Clerk to throw and return a 404 for unauthenticated requests, use `auth.protect()`:

```ts
// app/api/admin/route.ts
import { auth } from '@clerk/nextjs/server'

export async function GET() {
  const { userId, orgRole } = await auth.protect({ role: 'org:admin' })
  return Response.json({ userId, orgRole })
}
```

Do not set `export const runtime = 'edge'` in new route handlers. Vercel recommends migrating new work to Node.js; the Edge Runtime is still documented but flagged for migration ([Vercel Edge Functions deprecated](https://vercel.com/docs/functions/runtimes/edge/edge-functions)).

### Clerk on Cloudflare Workers (primary true-edge example)

`@clerk/backend` is the canonical SDK for Cloudflare Workers — there is no separate `@clerk/cloudflare-workers` package ([Clerk backend-only SDK development guide](https://clerk.com/docs/guides/development/sdk-development/backend-only.md)). The old `@clerk/edge` package is deprecated.

Two common shapes:

#### Hono + `@hono/clerk-auth`

```ts
// src/index.ts
import { Hono } from 'hono'
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'

type Bindings = {
  CLERK_PUBLISHABLE_KEY: string
  CLERK_SECRET_KEY: string
  CLERK_JWT_KEY: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.use('*', clerkMiddleware())

app.get('/me', (c) => {
  const auth = getAuth(c)
  if (!auth?.userId) {
    return c.json({ error: 'unauthorized' }, 401)
  }
  return c.json({ userId: auth.userId, orgId: auth.orgId })
})

export default app
```

The middleware picks up `CLERK_SECRET_KEY` and `CLERK_PUBLISHABLE_KEY` from the Worker's environment bindings ([`@hono/clerk-auth` source](https://github.com/honojs/middleware/tree/main/packages/clerk-auth)).

#### Raw Workers + `@clerk/backend`

```ts
// src/index.ts
import { createClerkClient } from '@clerk/backend'

type Env = {
  CLERK_SECRET_KEY: string
  CLERK_PUBLISHABLE_KEY: string
  CLERK_JWT_KEY: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const clerkClient = createClerkClient({
      secretKey: env.CLERK_SECRET_KEY,
      publishableKey: env.CLERK_PUBLISHABLE_KEY,
      jwtKey: env.CLERK_JWT_KEY,
    })

    const requestState = await clerkClient.authenticateRequest(request)

    if (!requestState.isAuthenticated) {
      return new Response('Unauthorized', { status: 401 })
    }

    const { userId } = requestState.toAuth()
    return Response.json({ userId, tokenType: requestState.tokenType })
  },
}
```

`authenticateRequest()` accepts a standard Web API `Request` object and returns a `RequestState` that exposes `isAuthenticated`, `status`, and `tokenType` ([Clerk `authenticateRequest()` reference](https://clerk.com/docs/references/backend/authenticate-request.md), [community tRPC + Clerk on Workers](https://dev.to/yinks/implementing-authorization-with-clerk-in-a-trpc-app-running-on-a-cloudflare-worker-4li5)).

Environment bindings are set via `wrangler secret put` or the Cloudflare dashboard ([Cloudflare Workers Secrets](https://developers.cloudflare.com/workers/configuration/secrets/)):

```bash
wrangler secret put CLERK_SECRET_KEY
wrangler secret put CLERK_JWT_KEY
```

`wrangler.toml` references the binding names in your `[vars]` / `[env.production.vars]` sections (secrets are injected separately, not written to `wrangler.toml`).

> Set `CLERK_JWT_KEY` to Clerk's PEM public key (Dashboard → API Keys → PEM Public Key) for zero-network-roundtrip session verification in Cloudflare Workers ([Clerk `verifyToken()` reference](https://clerk.com/docs/reference/backend/verify-token.md)).

### Machine-to-machine and backend-to-backend with Clerk

Clerk's machine-auth model distinguishes three approaches ([Clerk machine-auth overview](https://clerk.com/docs/guides/development/machine-auth/overview.md)):

- **OAuth access tokens** — on-behalf-of-user, issued by Clerk acting as an OAuth 2.0 / OIDC authorization server.
- **M2M tokens** — service-to-service. The recommended default for internal calls.
- **API keys** — long-lived, user- or org-delegated credentials.

While OAuth 2.0 client credentials grant is on Clerk's roadmap, use M2M tokens for service-to-service identity today.

**Token formats for M2M** ([Clerk M2M token formats](https://clerk.com/docs/guides/development/machine-auth/token-formats.md), [Clerk changelog — M2M JWT tokens](https://clerk.com/changelog/2026-02-24-m2m-jwt-tokens.md)):

- **JWT M2M tokens** (released February 24, 2026) — free to verify, networkless, cannot be revoked. Recommended default.
- **Opaque M2M tokens** — $0.00001 per verification, instantly revocable. Use for revocation-sensitive workloads.

Max 150 scopes per M2M token ([Clerk M2M tokens guide](https://clerk.com/docs/guides/development/machine-auth/m2m-tokens.md)).

Issuing a token from one service:

```ts
// services/payments/issue-m2m.ts
import { createClerkClient } from '@clerk/backend'

const clerkClient = createClerkClient({
  secretKey: process.env.CLERK_MACHINE_SECRET_KEY!,
})

export async function mintWorkerToken() {
  const token = await clerkClient.m2m.createToken({
    claims: { audience: 'https://api.example.com' },
    scopes: ['read:payments', 'write:invoices'],
    secondsUntilExpiration: 300, // 5 minutes
  })
  return token
}
```

Verifying on the receiving service:

```ts
// services/api/verify-m2m.ts
import { createClerkClient } from '@clerk/backend'

const clerkClient = createClerkClient({
  secretKey: process.env.CLERK_MACHINE_SECRET_KEY!,
})

export async function verifyM2M(bearerToken: string) {
  const result = await clerkClient.m2m.verify({ token: bearerToken })
  if (result.tokenType !== 'm2m_token') {
    throw new Error('Unexpected token type')
  }
  return { machineId: result.machineId, scopes: result.scopes }
}
```

`CLERK_MACHINE_SECRET_KEY` (prefixed `ak_`) is the dedicated key for machine auth operations. Keep it separate from your application's `CLERK_SECRET_KEY` so you can rotate independently.

### Multi-token acceptance pattern

Endpoints often serve multiple clients (web cookies, mobile Bearer tokens, third-party OAuth, internal M2M, API keys). Clerk's `acceptsToken` option collapses this into one call ([Clerk verifying API keys in Next.js](https://clerk.com/docs/guides/development/verifying-api-keys.md), [Clerk `authenticateRequest()` reference](https://clerk.com/docs/reference/backend/authenticate-request.md)).

```ts
// app/api/items/route.ts
import { auth } from '@clerk/nextjs/server'

export async function GET() {
  const result = await auth({
    acceptsToken: ['session_token', 'api_key', 'm2m_token', 'oauth_token'],
  })

  if (!result.isAuthenticated) {
    return Response.json({ error: 'unauthorized' }, { status: 401 })
  }

  switch (result.tokenType) {
    case 'session_token':
      return Response.json({ shape: 'user', userId: result.userId })
    case 'api_key':
      return Response.json({
        shape: 'api_key',
        subject: result.subject,
        scopes: result.scopes,
      })
    case 'm2m_token':
      return Response.json({
        shape: 'machine',
        machineId: result.machineId,
        scopes: result.scopes,
      })
    case 'oauth_token':
      return Response.json({
        shape: 'oauth',
        userId: result.userId,
        clientId: result.clientId,
      })
  }
}
```

`result.tokenType` tells downstream code which path to take without re-parsing the token.

### Session handling across distributed functions

Serverless functions cannot rely on shared in-memory sessions. Clerk's hybrid model handles this:

- `__client` cookie — long-lived identity on Clerk's Frontend API (FAPI) domain.
- `__session` cookie — a 60-second JWT scoped to the app domain, containing the current session token.
- Proactive refresh at 50 seconds via Clerk's frontend SDK — the token is refreshed in the background before it expires, so a long-running page never trips an expired-token state ([Clerk Core 3 changelog](https://clerk.com/changelog/2026-03-03-core-3.md), [Clerk session tokens reference](https://clerk.com/docs/guides/sessions/session-tokens.md)).

Session JWTs carry standard claims (`exp`, `sub`, `iss`, etc.) plus organization-scoped claims when active.

Long-running clients like `@clerk/expo` automatically handle refreshes via token caches.

> Clerk's `__session` cookie is intentionally **not `HttpOnly`**. The frontend SDK needs to read it to drive proactive refresh. Exposure is mitigated by the 60-second TTL — XSS exposure is under a minute before the token is rotated ([Clerk session tokens reference](https://clerk.com/docs/guides/sessions/session-tokens.md)).

### Monorepo setup with Clerk

The publishable key is the common thread, but env names and secrets differ by runtime. Server runtimes (`@clerk/nextjs` server, `@clerk/backend`) read `CLERK_SECRET_KEY` and `CLERK_JWT_KEY` alongside the publishable key; client runtimes take only the publishable key under a framework-specific prefix and never receive the secret or JWT key — `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` for `@clerk/nextjs` and `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` for `@clerk/expo`. Split the schema so each runtime validates only what belongs to it.

#### Shared auth package (config + types, not SDK wrapper)

```ts
// packages/auth-config/src/env.ts
import { z } from 'zod'

// Server runtimes: @clerk/nextjs (server) and @clerk/backend.
const serverSchema = z.object({
  CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
  CLERK_SECRET_KEY: z.string().startsWith('sk_'),
  CLERK_JWT_KEY: z.string().startsWith('-----BEGIN PUBLIC KEY-----'),
  CLERK_MACHINE_SECRET_KEY: z.string().startsWith('ak_').optional(),
})

// Expo client: publishable key only, under the EXPO_PUBLIC_ prefix.
const expoSchema = z.object({
  EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
})

export const serverAuthEnv = serverSchema.parse(process.env)
export const expoAuthEnv = expoSchema.parse(process.env)
export type ServerAuthEnv = z.infer<typeof serverSchema>
export type ExpoAuthEnv = z.infer<typeof expoSchema>
```

`turbo.json` declares auth env vars as global so cache keys include them:

```json
{
  "globalEnv": [
    "CLERK_PUBLISHABLE_KEY",
    "CLERK_SECRET_KEY",
    "CLERK_JWT_KEY",
    "CLERK_MACHINE_SECRET_KEY"
  ],
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
    "dev": { "cache": false, "persistent": true }
  }
}
```

Apps read `authEnv` and pass values to runtime-specific SDKs without shared wrappers.

#### Per-app handlers

- **Web app** (`apps/web`): `proxy.ts` with `clerkMiddleware()` + `ClerkProvider` at `app/layout.tsx` (see "Edge middleware with Clerk and Next.js 16" and "Installing and configuring Clerk" above).
- **API service** (`apps/api`, Cloudflare Workers): `@clerk/backend` + `@hono/clerk-auth` (see "Clerk on Cloudflare Workers" above).
- **Mobile** (`apps/mobile`, Expo): `@clerk/expo` with `<ClerkProvider>` + `tokenCache`.
- **Background workers** (`apps/workers`): `CLERK_MACHINE_SECRET_KEY` + `clerkClient.m2m.createToken()` for internal calls (see "Machine-to-machine and backend-to-backend with Clerk" above).

Each sub-app depends on `@your-org/auth-config` for env validation and shared types only.

### End-to-end example: monorepo with edge web app, cross-origin API, and background worker

For cross-origin setups (e.g., `app.example.com` and `api.example.com`), same-origin requests use the `__session` cookie; cross-origin requests use `Authorization: Bearer` with the JWT from `getToken()` ([Clerk making requests](https://clerk.com/docs/guides/development/making-requests.md), [Clerk cookies guide](https://clerk.com/docs/guides/how-clerk-works/cookies.md)).

Architecture (one transport per hop):

```
┌───────────────────┐                                ┌─────────────────────┐
│ Web (Next.js 16)  │  Authorization: Bearer <JWT>   │ API (CF Worker)     │
│ proxy.ts          │ ─────────────────────────────→ │ @clerk/backend      │
│ clerkMiddleware() │   (cross-origin: api.domain)   │ authenticateRequest │
└───────────────────┘                                └─────────┬───────────┘
                                                               │
┌───────────────────┐  Authorization: Bearer <JWT>             │
│ Mobile (Expo)     │ ───────────────────────────────────────→ │
│ @clerk/expo       │   (always cross-origin)                  │
└───────────────────┘                                          │
                                                               │
┌───────────────────┐  Authorization: Bearer <M2M JWT>         │
│ Background Worker │ ───────────────────────────────────────→ │
│ CLERK_MACHINE_    │   (service-to-service)                   │
│ SECRET_KEY        │                                          │
└───────────────────┘                                          ▼
                                                        tokenType switch:
                                                        session_token | m2m_token
```

- **User-identity hop (web → API, cross-origin)**. The frontend calls `getToken()` for the `__session` JWT, sending it via `Authorization: Bearer` to the API. The Worker verifies it networklessly via `jwtKey`.
- **User-identity hop (mobile → API)**. `@clerk/expo` caches and attaches the session token as `Authorization: Bearer`. Verification remains identical.
- **M2M hop (background worker → API)**. Workers use `CLERK_MACHINE_SECRET_KEY` to mint JWT M2M tokens, sending them via `Authorization: Bearer`. The API accepts both via `acceptsToken: ['session_token', 'm2m_token']`.

**Same-origin alternative.** For same-origin APIs (both at `example.com`), the `__session` cookie flows automatically to `authenticateRequest()` without Bearer headers. Choose the transport matching your deployment.

## Next Steps

In this second part, we covered the critical constraints of major serverless platforms, tackled the unique challenges of monorepo authentication architectures, and walked through a complete implementation utilizing Clerk Core 3 and Next.js 16. By pushing JWT verification to the edge and centralizing shared auth configuration, you can eliminate network round trips and avoid config drift across distributed applications.

In Part 3, we will shift focus to advanced use cases, including robust security patterns, session revocation in stateless systems, and managing edge authorization securely.

## Frequently Asked Questions

## FAQ

### Does Clerk work with Cloudflare Workers?

Yes. Cloudflare Workers is a fully supported V8-isolate edge target. You can use the canonical `@clerk/backend` SDK to authenticate requests natively on the edge. By configuring the `jwtKey` with Clerk's PEM public key, you achieve zero-network-round-trip verification directly within the Worker.

### Does Clerk work with Vercel Edge Functions and Next.js 16?

Yes, but keep in mind that Next.js 16's new `proxy.ts` runs exclusively on the Node.js runtime, not the Edge runtime. Clerk's `@clerk/nextjs` package seamlessly handles authentication in `proxy.ts` on Node.js. If you are using raw Vercel Edge Functions via Vercel Routing Middleware, Clerk fully supports the edge runtime there as well, though Vercel generally recommends migrating new work to Node.js.

### How do you authenticate service-to-service calls in a serverless architecture?

The recommended approach for internal, backend-to-backend communication is using Machine-to-Machine (M2M) tokens. For example, a background worker can use Clerk's `CLERK_MACHINE_SECRET_KEY` to mint a short-lived JWT M2M token. The worker passes this token as a Bearer header to the destination API, which verifies the token structure and scopes before authorizing the action.

## In this series

1. [Authentication for Serverless and Edge Deployments](https://clerk.com/articles/authentication-for-serverless-and-edge-deployments.md)
2. **Authentication for Serverless and Edge Deployments - Part 2** (you are here)
3. [Authentication for Serverless and Edge Deployments - Part 3](https://clerk.com/articles/authentication-for-serverless-and-edge-deployments-3.md)
