Skip to content

Target Architecture (spike/cf-runtime)

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.

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-types for type compatibility
  • wrangler configuration in wrangler.jsonc and wrangler.toml

The database driver switches from pg (Node.js TCP pool) to @neondatabase/serverless (Neon HTTP driver):

  • src/db/index.ts uses neon() + drizzle-orm/neon-http instead of pg.Pool
  • Each query is a separate HTTP round-trip (stateless)
  • SET LOCAL GUCs for RLS require explicit withTenantSession transactions
  • No persistent connection pool — ideal for serverless/edge runtimes

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.astro and src/layouts/OperatorLayout.astro — Dedicated layouts
  • src/components/islands/ — Interactive CRM components (UnitsTable, AgentManager, AdminProjectSwitcher)

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, withTenantSession
  • src/lib/tenant/resolve-host.ts — Hostname-based tenant resolution with caching
  • src/lib/db/tenant-context.ts — Context building from Lucia user attributes
  • Drizzle migrations add project_id columns to all tenant-scoped tables
  • RLS policies enforce isolation at the database level (defense in depth)
ModulePurpose
src/lib/cloudflare/custom-hostnames.tsCloudflare for SaaS API wrapper
src/lib/auth/admin-lucia.tsSeparate Lucia instance for CRM sessions
src/lib/auth/admin-session.tsCRM session validation and helpers
src/lib/auth/cf-access.tsCloudflare Access JWT verification
src/lib/audit/index.tsImmutable audit event logging
src/lib/r2/R2 presigned URL generation
src/lib/tenant/Hostname resolution and tenant context

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