Architecture Overview
Application shape
Legal Hub is a Cloudflare-native full-stack application: one Worker serves the Hono API and the React 19 / Vite SPA through Workers Assets. An independent Astro marketing site lives undermarketing/ for Cloudflare Pages deployment.
Request and access path
The Worker applies request IDs, logging, security headers, CORS, error handling, and request telemetry before routing. Public configuration, contact, signup, authentication, brand lookup, webhook, and health routes are mounted separately from authenticated tenant routes. The SPA starts Auth0 Universal Login with Authorization Code and PKCE. The callback verifies the Auth0 ID token against JWKS, then Legal Hub creates its own HS256 signed session set in the HttpOnly, Secure, SameSite=Laxpcnaid_access_token cookie. No Auth0 refresh token is requested or stored.
Authenticated API requests pass through authentication, CSRF, and tenant middleware. The selected organization is sent as X-Organization-Id; users with multiple active memberships must identify the organization. Individual route handlers then apply role and permission checks. Client portal routes also restrict records to explicit client access grants.
First-run onboarding serializes the authenticated user’s membership check and organization creation within a transaction. Zero-membership users are routed via RequireFirm to /onboarding where they bootstrap their firm and initiate Stripe Checkout.
Audience routing resolves client-only users to /portal, firm users to the firm dashboard, and Pcnaid staff to /admin.
Data model and tenant isolation
Each logical collection has its own Postgres table mapped byCOLLECTION_TABLE in src/worker/db/connection.ts, storing an ID, organization ID, JSONB payload, and creation/update/deletion timestamps. The Worker compatibility layer preserves the query API used by routes and repositories while persisting through Postgres/Hyperdrive.
Every tenant-scoped record includes organizationId. Repositories add deletedAt: null and organization filters for normal reads and writes. The adapter reads the request’s tenant context and sets transaction-local app.org_id and app.access for enforced row-level security. Tenant SQL can read its own organization and user identities referenced by that organization’s non-deleted memberships.
knowledge_chunk_embeddings is a separate pgvector-backed semantic index for tenant-scoped AI retrieval. The legacy app_records table is retained as a historical source; normal collection operations use the per-entity tables.
Cloudflare services
The Worker configuration declares:- Hyperdrive for Postgres access.
- KV for tenant-context caching, upload tracking, and small idempotency or state records.
- R2 buckets for documents (
legal-hub-documents) and audit-log objects (legal-hub-audit-logs). - Queues for tasks, email, SMS, document processing, and AI work, each with a dead-letter queue.
- Scheduled handler for stale-upload cleanup, calendar OAuth cleanup, and reminders.
- Workers AI binding with AI provider settings supplied through runtime configuration.
Auth and tenant resolution
- React starts Auth0 Universal Login with PKCE (
screen_hint=signupfor signups, preserving optionalplanin sessionStorage). - Auth0 redirects to
/auth/callback. - The Worker exchanges the authorization code at
/api/auth/callback. - The Worker verifies the Auth0 ID token through JWKS, then mints its own HS256 app-session JWT and sets it in the HttpOnly, Secure, SameSite=Lax
pcnaid_access_tokencookie. - API calls use same-origin cookies; no localStorage JWTs are stored or attached.
- Worker middleware verifies the app-session JWT, finds the user by
auth0UserId, and resolves memberships. - Zero-membership users are routed to
/onboarding. - For active organizations, the frontend sends
X-Organization-Id; the backend rejects ambiguous multi-org requests without it.
Async processing
Queue handlers normalize message shapes and use Redis REST idempotency when available; otherwise async queue idempotency can fall back to Cloudflare KV. Request rate limits and AI usage counters require Redis in staging/production and fail closed if Redis is absent.
AI/RAG
Knowledge chunks are stored tenant-scoped in Postgres./api/ai/query embeds the user query, retrieves semantically similar chunks from knowledge_chunk_embeddings using pgvector, wraps context as untrusted data, calls OpenAI Responses API when configured, records the AI run, and returns provider metadata. A bounded JSON cosine fallback is available if the pgvector query path is unavailable during local development.
Uploads and audit logs
Documents live in R2 under organization-scoped keys and complete only after SHA-256 checksum verification. The verifier uses R2 checksum metadata when present and falls back to server-side streaming SHA-256 over the completed object. Multipart upload state is tracked in KV, and a scheduled Worker aborts stale uploads. Sensitive mutations write normal DB audit records and, whenAUDIT_LOGS_BUCKET is bound, an additional JSON copy to R2 under audit-logs/YYYY/MM/DD/....
Observability
- Sentry wraps the Worker export and captures 500-level errors with request IDs.
- PostHog records API request events with method, path, status, latency, and tenant correlation where available.
- Cloudflare Observability is enabled in
wrangler.jsonc. - Audit logs persist sensitive application events and can be mirrored to R2.
Build and deployment
npm run build builds the SPA and runs wrangler deploy --dry-run; npm run deploy builds the SPA and deploys the Worker. Runtime bindings and non-secret defaults are declared in wrangler.jsonc. Production deployment requires real provider credentials and replacement Cloudflare binding IDs in wrangler.jsonc before npm run deploy.