Backend JSRuntime GuidesServerless Workers

Serverless Platform Patterns

Platform-ready auth and backend SDK patterns for Lambda-style and edge-worker runtimes.

Use one runtime-agnostic handler shape and adapt platform event details at the edge of your app.
This pattern covers both auth modes for fully serverless backend deployments:

  • JWT/session auth for user-scoped routes
  • API key/OAuth protected auth for machine routes

Shared auth handlers

src/handlers/users.ts
import { authenticateRequest, gateway, users, WachtAuthError } from '@wacht/backend';

export async function handleUserRead(request: Request): Promise<Response> {
  try {
    const { auth } = await authenticateRequest(request);
    await auth.protect({ permission: 'user:read' });
    return Response.json(await users.listUsers({ limit: 20 }));
  } catch (error) {
    if (error instanceof WachtAuthError) {
      return Response.json({ error: error.code, message: error.message }, { status: error.status });
    }
    return Response.json({ error: 'internal_error' }, { status: 500 });
  }
}

export async function handleMachineUserRead(request: Request): Promise<Response> {
  const decision = await gateway.checkPrincipalAuthz({
    principalType: 'api_key',
    principalValue: request.headers.get('x-api-key') ?? '',
    resource: '/machine/users',
    method: 'GET',
    requiredPermissions: ['user:read'],
  });

  if (!decision.allowed) {
    return new Response(JSON.stringify({ error: 'forbidden' }), { status: 403 });
  }

  return Response.json(await users.listUsers({ limit: 20 }));
}

Route dispatcher (both auth modes)

src/handlers/router.ts
import { handleMachineUserRead, handleUserRead } from './users';

export async function routeRequest(request: Request): Promise<Response> {
  const url = new URL(request.url);

  if (url.pathname === '/admin/users' && request.method === 'GET') {
    return handleUserRead(request);
  }

  if (url.pathname === '/machine/users' && request.method === 'GET') {
    return handleMachineUserRead(request);
  }

  return new Response(JSON.stringify({ error: 'not_found' }), { status: 404 });
}

AWS Lambda adapter (JWT + machine)

src/adapters/lambda.ts
import { initClient } from '@wacht/backend';
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { routeRequest } from '../handlers/router';

export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
  initClient({
    apiKey: process.env.WACHT_API_KEY!,
    baseUrl: process.env.WACHT_BACKEND_API_URL,
  });

  const request = new Request(`https://lambda.local${event.rawPath}`, {
    method: event.requestContext.http.method,
    headers: event.headers as HeadersInit,
  });

  const response = await routeRequest(request);
  return {
    statusCode: response.status,
    headers: Object.fromEntries(response.headers.entries()),
    body: await response.text(),
  };
}

Edge adapter (JWT + machine)

src/adapters/edge.ts
import { initClient } from '@wacht/backend';
import { routeRequest } from '../handlers/router';

export default async function handler(request: Request): Promise<Response> {
  initClient({
    apiKey: process.env.WACHT_API_KEY!,
    baseUrl: process.env.WACHT_BACKEND_API_URL,
  });

  return routeRequest(request);
}

publishableKey is optional when WACHT_PUBLISHABLE_KEY is already configured in env.

On this page