Skip to content

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 logic
const authGuard = requireAuth(authContext);
if (authGuard) return authGuard;
// 2. Rate limiting — enforce per-tier limits
const 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 Zod
const parsed = YourInputSchema.safeParse(await request.json());
if (!parsed.success) {
return Response.json({ error: 'Invalid input', details: parsed.error.issues }, { status: 400 });
}
// 4. Execute business logic
const result = await doWork(parsed.data);
// 5. Response — CORS is applied automatically by the fetch() wrapper
return Response.json(result);

CORS: Which Function to Use

ScenarioFunctionReturns
Public read-only endpointgetPublicCorsHeaders()Access-Control-Allow-Origin: *
Authenticated/write endpointgetCorsHeaders(request, env)Origin-reflected from allowlist
OPTIONS preflighthandleCorsPreflight(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:

// ✅ Correct
const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// ❌ Wrong — SQL injection risk
const 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 pipe
return this.http.get<unknown>(url).pipe(
map(raw => validateResponse(YourResponseSchema, raw, 'YourService.method'))
);
// With async/await
const 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 compatibility

Secrets Management

Value TypeStorageExample
Secret / credentialwrangler secret putCLERK_SECRET_KEY, ADMIN_KEY
Public configwrangler.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 SDK
const token = await clerk.session?.getToken();
// ❌ Wrong — never store tokens manually
localStorage.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 user
const 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 existence
if (!row) {
return JsonResponse.notFound('Not found');
}
// ❌ Wrong — unscoped lookup; any user can access any key by ID
const 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

  1. Forgetting requireAuth() — Every write endpoint needs an auth gate
  2. Using Response.json() with manual CORS — The fetch() wrapper adds CORS; don’t duplicate
  3. Flushing empty objects in tests — Zod validation means mock data must match schemas
  4. String interpolation in SQL — Always .prepare().bind()
  5. Storing auth state in components — Use ClerkService signals
  6. Unscoped resource queries — Always bind authContext.userId in WHERE clauses on user-owned tables