API Security Model
Overview
Section titled βOverviewβapps/webapp uses a defense-in-depth authentication model for /api/* routes implemented across three layers:
- Explicit route-prefix categorization in
apps/webapp/src/middleware.tsβ categorizes routes into Public, Admin, Organization-Required, and Auth-Only prefixes. - Uncategorized-route enforcement in
apps/webapp/src/middleware/api-auth-enforcement.tsβ evaluates any/api/*route againstapps/webapp/src/lib/api/auth-allowlist.ts(PUBLIC_API_EXACT_PATHSandPUBLIC_API_PREFIX_PATHS). - Handler-level guards in
BaseAPIHandler(apps/webapp/src/lib/api/base-handler.ts) β route handlers explicitly callrequireAuth(),requireAdmin(),requireOrganization(), orvalidateInternalServiceToken().
Route Categories in middleware.ts
Section titled βRoute Categories in middleware.tsβ1. Public API Routes (PUBLIC_API_ROUTES & auth-allowlist.ts)
Section titled β1. Public API Routes (PUBLIC_API_ROUTES & auth-allowlist.ts)βRoutes that handle their own authentication (e.g., webhook signatures, service tokens, OAuth 2.1 / PKCE flows) or are intentionally public health probes:
/api/health(and/api/health/ready,/api/health/redis,/api/health/schema) β infrastructure & schema health probes/api/contactβ public contact form submissions/api/webhooks&/api/webhook/*β Clerk, Stripe, inbound email, forwarding confirmation, and Zendesk webhooks (verified via HMAC signatures orvalidateInternalServiceToken)/.well-known/*,/api/oauth/*,/api/mcp/*β OAuth 2.1 Authorization Server metadata, JWKS, Dynamic Client Registration, and token endpoints forops-mcp- Internal service-token routes in
PUBLIC_API_PREFIX_PATHS(/api/alias/,/api/email/triage,/api/eval/,/api/inbox-routing/internal/,/api/orders/missed-reconciliation/,/api/billing/internal/) β guarded by header secrets viavalidateInternalServiceToken
2. Admin API Routes (ADMIN_API_ROUTES)
Section titled β2. Admin API Routes (ADMIN_API_ROUTES)βRequire an authenticated Clerk session belonging to a system administrator (isUserAdmin(user)):
/api/admin/*β system administration/api/sys-admin/*β platform operator controls/api/cache/*β Redis cache inspection and flushing/api/maintenance/*β maintenance operations/api/debug/*β diagnostic endpoints/api/tests/*β test harness endpoints
3. Organization-Required API Routes (ORG_REQUIRED_API_ROUTES)
Section titled β3. Organization-Required API Routes (ORG_REQUIRED_API_ROUTES)βRequire both an authenticated Clerk user session and an active organization context (clerk_organization_id), verified against organization membership:
/api/erp/*β ERP connections, catalog search, quote generation, and order submission/api/orders/*β order CRUD, revisions, and feedback/api/extracted-orders/*β extracted order review state/api/pdf-documents/*β document upload and retrieval. Exception:POST /api/pdf-documentsitself is runtime-public (apps/webapp/src/lib/api/auth-allowlist.ts) because it is mixed-auth:source=manualcallsrequireAuthFromLocals, whilesource=external(the Cloudflare email worker) has no Clerk session and is authenticated by anX-Inbound-Email-SignatureHMAC over{filename, connectionId, organizationId}, whoseorganizationIdmust own the connection. Sub-routes such as/api/pdf-documents/[id]are not allowlisted and do require a session./api/inbox/*β unified inbox queries and bulk actions/api/emails/*β email threads, connections, and labels/api/alias/*β organization email aliases/api/billing/*&/api/subscriptions/*β Stripe billing and usage analytics/api/organizations/*&/api/settings/org-rules/*β organization settings and routing rules/api/temporal/workflow/*β workflow status polling/api/analytics/*&/api/analytics-admin/*β product and operational metrics/api/audit/*,/api/notifications/*,/api/support/*
4. Auth-Only API Routes (AUTH_ONLY_API_ROUTES)
Section titled β4. Auth-Only API Routes (AUTH_ONLY_API_ROUTES)βRequire an authenticated Clerk user session, with organization scoping resolved inside the handler:
/api/auth/*β session and user profile endpoints/api/work-items/*β work-item state transitions and snooze controls
Uncategorized API Route Enforcement (AUTH_ENFORCEMENT_MODE)
Section titled βUncategorized API Route Enforcement (AUTH_ENFORCEMENT_MODE)βapps/webapp/src/middleware/api-auth-enforcement.ts runs on every /api/* request not matched by PUBLIC_API_EXACT_PATHS or PUBLIC_API_PREFIX_PATHS:
AUTH_ENFORCEMENT_MODE=log(current default): If no authenticatedlocals.userIdis present, the middleware logs a structured warning (AUTH_ENFORCEMENT: would block) with{ path, method }and passes the request to the route handler (return next()).AUTH_ENFORCEMENT_MODE=enforce: Immediately rejects unauthenticated requests at the middleware boundary with HTTP401:{"error": "Unauthorized"}AUTH_ENFORCEMENT_MODE=off: Disables the enforcement check.
Handler Implementation Patterns
Section titled βHandler Implementation PatternsβAdmin Endpoints
Section titled βAdmin Endpointsβclass AdminHandler extends BaseAPIHandler { async GET({ locals, request }: APIContext): Promise<Response> { return this.handleRequest(async () => { const { userId } = await this.requireAdmin(locals, request); // Admin-only logic... }); }}Organization-Scoped Endpoints
Section titled βOrganization-Scoped Endpointsβclass OrderHandler extends BaseAPIHandler { async POST({ locals, request }: APIContext): Promise<Response> { return this.handleRequest(async () => { const { userId, organizationId } = await this.requireOrganization(locals, request); // Every DB query MUST filter by organizationId (clerk_organization_id) }); }}Internal Service-to-Service Endpoints
Section titled βInternal Service-to-Service EndpointsβRoutes called by temporal-worker, ts-temporal-worker, or email-worker must validate a shared service token using constant-time comparison (validateInternalServiceToken in apps/webapp/src/lib/api/validate-internal-token.ts):
import { validateInternalServiceToken } from '@/lib/api/validate-internal-token';
const authResult = validateInternalServiceToken(request, process.env.INTERNAL_SERVICE_TOKEN);if (!authResult.valid) { return new Response(JSON.stringify({ error: authResult.error }), { status: authResult.status });}Adding a New API Route Checklist
Section titled βAdding a New API Route Checklistβ- Choose the auth model:
- User-facing tenant route β add prefix to
ORG_REQUIRED_API_ROUTESinapps/webapp/src/middleware.tsand callrequireAuth/requireOrganizationin the handler. - System admin route β add prefix to
ADMIN_API_ROUTESand callrequireAdmin. - Worker/webhook route β if it uses its own signature or service token, register the path in
apps/webapp/src/lib/api/auth-allowlist.tsand verify the signature/token at the top of the handler.
- User-facing tenant route β add prefix to
- Scope every query by tenant: Always include
eq(table.clerkOrganizationId, organizationId)oreq(table.connectionId, connectionId). - Add tests: Include an unauthenticated
401test and a cross-organization403/404isolation test.