ZTA Developer Guide
ZTA Developer Guide
Practical guide for contributors working on the bloqr-backend. Follow these patterns to maintain Zero Trust Architecture compliance.
Adding a New Worker Endpoint
Every new endpoint must follow this pattern:
// 1. Auth gate — verify before any business logicconst authGuard = requireAuth(authContext);if (authGuard) return authGuard;
// 2. Rate limiting — enforce per-tier limitsconst rateLimit = await checkRateLimitTiered(env, ip, authContext);if (!rateLimit.allowed) { analytics.trackSecurityEvent({ eventType: 'rate_limit', path: '/your/endpoint', method: request.method, clientIpHash: AnalyticsService.hashIp(ip), tier: authContext.tier, reason: 'rate_limit_exceeded', }); return Response.json({ error: 'Rate limit exceeded' }, { status: 429 });}
// 3. Validate input with Zodconst parsed = YourInputSchema.safeParse(await request.json());if (!parsed.success) { return Response.json({ error: 'Invalid input', details: parsed.error.issues }, { status: 400 });}
// 4. Execute business logicconst result = await doWork(parsed.data);
// 5. Response — CORS is applied automatically by the fetch() wrapperreturn Response.json(result);CORS: Which Function to Use
| Scenario | Function | Returns |
|---|---|---|
| Public read-only endpoint | getPublicCorsHeaders() | Access-Control-Allow-Origin: * |
| Authenticated/write endpoint | getCorsHeaders(request, env) | Origin-reflected from allowlist |
| OPTIONS preflight | handleCorsPreflight(request, env) | Full preflight response |
Never add Access-Control-Allow-Origin: * directly. Import from worker/utils/cors.ts.
D1 Database Queries
Always use parameterized queries:
// ✅ Correctconst result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// ❌ Wrong — SQL injection riskconst result = await env.DB.prepare(`SELECT * FROM users WHERE id = '${userId}'`).first();Frontend API Consumption
All API responses must be Zod-validated before use:
import { validateResponse } from '../schemas/api-responses';import { YourResponseSchema } from '../schemas/api-responses';
// In an Observable pipereturn this.http.get<unknown>(url).pipe( map(raw => validateResponse(YourResponseSchema, raw, 'YourService.method')));
// With async/awaitconst raw = await firstValueFrom(this.http.get<unknown>(url));return validateResponse(YourResponseSchema, raw, 'YourService.method');Adding Zod Schemas
Add new schemas to frontend/src/app/schemas/api-responses.ts:
export const NewResponseSchema = z.object({ success: z.boolean(), data: z.object({ // your fields }),}).passthrough(); // Allow extra fields for forward compatibilitySecrets Management
| Value Type | Storage | Example |
|---|---|---|
| Secret / credential | wrangler secret put | CLERK_SECRET_KEY, ADMIN_KEY |
| Public config | wrangler.toml [vars] | CORS_ALLOWED_ORIGINS, COMPILER_VERSION |
| Local dev | .env.local (gitignored) | All values |
Never commit secrets to source or put them in wrangler.toml [vars].
Auth Token Management (Frontend)
// ✅ Correct — use Clerk SDKconst token = await clerk.session?.getToken();
// ❌ Wrong — never store tokens manuallylocalStorage.setItem('token', jwt);The HTTP interceptor (auth.interceptor.ts) automatically attaches Bearer tokens. Do not attach tokens manually in service code.
PR Checklist
Every PR touching worker/ or frontend/ must complete the ZTA checklist in the PR template. The CI zta-lint workflow runs automated checks, but the checklist covers items that require human review.
PRs touching docs/api/openapi.yaml or resource endpoint handlers (endpoints with path parameters like /{id}) must also complete the API Shield / Vulnerability Scanner section of the PR template.
BOLA Prevention (Broken Object Level Authorization)
The API Shield Vulnerability Scanner detects BOLA — accessing another user’s resources via ID manipulation. Every resource endpoint must scope queries to the authenticated user:
// ✅ Correct — query scoped to authenticated userconst row = await env.DB .prepare('SELECT * FROM api_keys WHERE user_id = ? AND id = ?') .bind(authContext.userId, keyId) .first();
// Return 404 (not 403) if not found — avoid leaking resource existenceif (!row) { return JsonResponse.notFound('Not found');}
// ❌ Wrong — unscoped lookup; any user can access any key by IDconst row = await env.DB .prepare('SELECT * FROM api_keys WHERE id = ?') .bind(keyId) .first();Why 404 and not 403? Returning 403 when a resource exists (but belongs to another user) leaks the existence of that resource. Always return 404 for missing-or-unauthorized resources on user-scoped endpoints.
See API Shield Vulnerability Scanner for the full guide and CI setup.
Common Mistakes
- Forgetting
requireAuth()— Every write endpoint needs an auth gate - Using
Response.json()with manual CORS — Thefetch()wrapper adds CORS; don’t duplicate - Flushing empty objects in tests — Zod validation means mock data must match schemas
- String interpolation in SQL — Always
.prepare().bind() - Storing auth state in components — Use
ClerkServicesignals - Unscoped resource queries — Always bind
authContext.userIdin WHERE clauses on user-owned tables