
Authentication for Serverless and Edge Deployments - Part 2
Part 2 of 3. Start with Authentication for Serverless and Edge Deployments.
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, Next.js 16 release blog).
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, Vercel Edge Functions deprecated).
Vercel Routing Middleware (platform-layer, distinct from proxy.ts) still uses the Edge runtime and is not deprecated (Vercel Routing Middleware docs, Vercel changelog unification).
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, How Fluid Compute Works, Vercel Active CPU pricing).
Node runtime bundle limit is 250MB; memory up to 4GB; max duration 300–800s (Vercel Functions limitations).
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).
- Worker Sharding — Cloudflare reports approximately 90% reduction in evictions with sharding; Workers start in under 5ms (ByteByteGo — How Cloudflare eliminates cold starts).
- KV for shared JWKS/user cache — hot key reads
<1ms; p99<5ms(since Oct 2025 update) (Cloudflare KV performance). - Durable Objects — single-instance pinning for rate limiting or strongly consistent session state (Durable Objects).
nodejs_compatflag — compat date 2024-09-23+ enables Node-stylecrypto,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).- Hono is a common Workers framework with an official Clerk middleware 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, edgedelta AWS Lambda cold start costs).
- 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).
- API Gateway JWT authorizer (native, no Lambda invocation) vs. Lambda authorizer (REQUEST- or TOKEN-based, cacheable, custom logic) (API Gateway JWT authorizer, Lambda authorizer).
- SnapStart was expanded to Python and .NET at re:Invent 2025; Node.js is not yet supported (AWS Lambda SnapStart docs).
Netlify Edge Functions
- Deno-based; 20MB compressed bundle; 512MB memory; 50ms CPU per request; 40s response header timeout (Netlify Edge Functions overview, Netlify Edge limits).
- Web Crypto and
node:prefix modules are supported. - No cross-subdomain cookies on
.netlify.appdomains. - Netlify lacks an official Clerk Edge integration; the documented path is SPA/React client-side only (Netlify Clerk integration page).
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, Deno Deploy as OIDC Provider).
- Deno + JWT via Web Crypto:
djwtnative library orjosenpm; both useSubtleCryptofor RS256 (Deno JWT with Web Crypto). - Bun: not an edge runtime. Public beta as a Vercel Node runtime option (Vercel Bun runtime docs); Vercel reports approximately 28% latency reduction for CPU-bound Next.js rendering vs. Node.js (Vercel Bun public beta).
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
globalEnvensures that auth env vars are part of the cache key and do not leak between tasks that expect different secrets (Turborepo environment variables, Turborepo configuration reference). - Centralize type definitions and utility functions in a shared internal package (e.g.,
packages/auth-config). This housesAppUser,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 so every app stays in lockstep on core auth dependencies.
References: Clerk T3 Turbo blog post, Clerk T3 Turbo GitHub, Convex + Turborepo + Clerk 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, refresh tokens, device-bound flows.
- Expo / React Native: secure token storage via
expo-secure-storeor 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 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, 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, OpenTelemetry baggage).
- Service-mesh variants — Istio
RequestAuthentication, Linkerd mTLS, Consul Connect — push JWT validation and workload identity into the data plane (Istio RequestAuthentication, Linkerd automatic mTLS, Consul Connect data plane). - Workload identity, not user identity, is covered by SPIFFE/SPIRE SVIDs and complements JWT user identity in zero-trust meshes (SPIFFE/SPIRE overview).
Choosing centralized authorization engines (Oso, OPA, Cerbos) vs. per-service logic is a separate decision. See Oso microservices authorization patterns and Contentstack monolith → microservices auth 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).
Why Clerk fits serverless and edge
- Edge-compatible SDKs with unified configuration.
@clerk/backendis the canonical SDK for Node.js ≥20.9.0 and V8 isolates (Cloudflare Workers, Vercel Edge) (Clerk Backend SDK overview, Clerk Backend-Only SDK guide). OneCLERK_PUBLISHABLE_KEYandCLERK_SECRET_KEYpair serves every runtime (@clerk/nextjs,@clerk/backend,@clerk/expo) without separate edge/node configs. - Networkless JWT verification. Configure
jwtKeywith Clerk's PEM public key for zero-network-round-trip verification. (ClerkverifyToken()reference, Clerk manual JWT verification). - 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). These community-sourced numbers illustrate order-of-magnitude improvements. Clerk's authoritative backbone remains networklessjwtKeyverification and a 60-second session TTL.
Installing and configuring Clerk (Next.js 16)
Required environment variables:
CLERK_PUBLISHABLE_KEY— the publishable key from the Clerk dashboard.CLERK_SECRET_KEY— the secret key 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:
pnpm add @clerk/nextjsThis 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, Clerk Core 3 release).
Wrap the app root with ClerkProvider:
// 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).
// 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:
// 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, Clerk Backend API rate limits). In high-volume edge settings, reserve currentUser() for necessary profile fields (emailAddresses, publicMetadata) and prefer useUser() on the client for display.
// 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). Add custom session claims via JWT templates so auth() carries commonly-read fields without Backend API calls.
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.
// 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():
// 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).
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). The old @clerk/edge package is deprecated.
Two common shapes:
Hono + @hono/clerk-auth
// 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 appThe middleware picks up CLERK_SECRET_KEY and CLERK_PUBLISHABLE_KEY from the Worker's environment bindings (@hono/clerk-auth source).
Raw Workers + @clerk/backend
// 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, community tRPC + Clerk on Workers).
Environment bindings are set via wrangler secret put or the Cloudflare dashboard (Cloudflare Workers Secrets):
wrangler secret put CLERK_SECRET_KEY
wrangler secret put CLERK_JWT_KEYwrangler.toml references the binding names in your [vars] / [env.production.vars] sections (secrets are injected separately, not written to wrangler.toml).
Machine-to-machine and backend-to-backend with Clerk
Clerk's machine-auth model distinguishes three approaches (Clerk machine-auth overview):
- 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, Clerk changelog — M2M JWT tokens):
- 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).
Issuing a token from one service:
// 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:
// 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, Clerk authenticateRequest() reference).
// 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:
__clientcookie — long-lived identity on Clerk's Frontend API (FAPI) domain.__sessioncookie — 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, Clerk session tokens reference).
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.
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)
// 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:
{
"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.tswithclerkMiddleware()+ClerkProvideratapp/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/expowith<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, Clerk cookies guide).
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__sessionJWT, sending it viaAuthorization: Bearerto the API. The Worker verifies it networklessly viajwtKey. - User-identity hop (mobile → API).
@clerk/expocaches and attaches the session token asAuthorization: Bearer. Verification remains identical. - M2M hop (background worker → API). Workers use
CLERK_MACHINE_SECRET_KEYto mint JWT M2M tokens, sending them viaAuthorization: Bearer. The API accepts both viaacceptsToken: ['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
In this series
- Authentication for Serverless and Edge Deployments
- Authentication for Serverless and Edge Deployments - Part 2 (you are here)
- Authentication for Serverless and Edge Deployments - Part 3