Target Architecture (spike/cf-runtime)
Overview
Section titled “Overview”The spike/cf-runtime branch implements a multi-tenant, single-deployment architecture. One buyer portal app and one CRM app serve all clients, with tenant context resolved per-request.
Key Changes from main
Section titled “Key Changes from main”Runtime: Node.js → Cloudflare Workers
Section titled “Runtime: Node.js → Cloudflare Workers”The Astro adapter changes from @astrojs/node to @astrojs/cloudflare, deploying to Cloudflare Pages with Workers runtime instead of a Node.js server.
- astro.config.mjs uses
cloudflare({ imageService: 'passthrough' }) - Build output targets
@cloudflare/workers-typesfor type compatibility - wrangler configuration in
wrangler.jsoncandwrangler.toml
Database: pg → Neon HTTP
Section titled “Database: pg → Neon HTTP”The database driver switches from pg (Node.js TCP pool) to @neondatabase/serverless (Neon HTTP driver):
src/db/index.tsusesneon()+drizzle-orm/neon-httpinstead ofpg.Pool- Each query is a separate HTTP round-trip (stateless)
SET LOCALGUCs for RLS require explicitwithTenantSessiontransactions- No persistent connection pool — ideal for serverless/edge runtimes
CRM: Directus → Built-in CRM
Section titled “CRM: Directus → Built-in CRM”The CRM surface moves from Directus containers to Astro pages within the same project:
src/pages/admin/— CRM routes (units, agents, leads, portal settings)src/pages/operator/— Operator-only routes (projects, organizations, domains)src/layouts/AdminLayout.astroandsrc/layouts/OperatorLayout.astro— Dedicated layoutssrc/components/islands/— Interactive CRM components (UnitsTable, AgentManager, AdminProjectSwitcher)
File Storage: Directus → Cloudflare R2
Section titled “File Storage: Directus → Cloudflare R2”File storage moves from Directus-managed uploads to direct Cloudflare R2 with presigned URLs:
src/lib/r2/— S3 client configured against R2 endpoint- Browser uploads directly to R2 via presigned PUT URLs (no server proxy)
- Document downloads use presigned GET URLs with TTL
- AWS SDK v3 (
@aws-sdk/client-s3,@aws-sdk/s3-request-presigner)
Tenant Scoping: Separate Databases → RLS
Section titled “Tenant Scoping: Separate Databases → RLS”Data isolation moves from per-client databases to a shared database with row-level security:
src/lib/db/scoped.ts—TenantContext,withTenantFilter,withProjectScope,withTenantSessionsrc/lib/tenant/resolve-host.ts— Hostname-based tenant resolution with cachingsrc/lib/db/tenant-context.ts— Context building from Lucia user attributes- Drizzle migrations add
project_idcolumns to all tenant-scoped tables - RLS policies enforce isolation at the database level (defense in depth)
New Modules Added
Section titled “New Modules Added”| Module | Purpose |
|---|---|
src/lib/cloudflare/custom-hostnames.ts | Cloudflare for SaaS API wrapper |
src/lib/auth/admin-lucia.ts | Separate Lucia instance for CRM sessions |
src/lib/auth/admin-session.ts | CRM session validation and helpers |
src/lib/auth/cf-access.ts | Cloudflare Access JWT verification |
src/lib/audit/index.ts | Immutable audit event logging |
src/lib/r2/ | R2 presigned URL generation |
src/lib/tenant/ | Hostname resolution and tenant context |
Migration Status
Section titled “Migration Status”The spike/cf-runtime branch has:
- ✅ All 18 Drizzle migrations (including multi-tenant schema)
- ✅ CRM/operator page routes and API endpoints
- ✅ Middleware dispatching for buyer/admin/operator paths
- ✅ Auth boundaries (buyer Lucia + CRM Lucia + CF Access)
- ✅ Tenant context plumbing (
withTenantSession, RLS, hostname resolution) - ✅ Audit logging framework
- ✅ R2 document storage (presigned URLs)
- ✅ Cloudflare for SaaS hostname management
- ✅ Test suite for multi-tenant isolation
- 🔄 Directus still runs read-only during migration
- ⏳ Phase 4 CRM refinement and testing