Key Concepts
White-Label Buyer Portal
Section titled “White-Label Buyer Portal”Each real estate project gets its own branded portal (custom domain, logo, colors) where buyers browse units, register, and contact agents. The portal is the primary buyer-facing surface.
Multi-Tenancy
Section titled “Multi-Tenancy”The platform serves multiple real estate developers (“clients”) from shared infrastructure. The tenant hierarchy is:
Organization (e.g. "Acme Bauträger GmbH") └── Project (e.g. "Sonnenhof Wien 22") └── Portal (branding + config) └── Portal Domains (sonnenhof.at, www.sonnenhof.at)Every database query is scoped to a project via tenant context.
Two Branch Story
Section titled “Two Branch Story”The main branch uses per-client infrastructure. The spike/cf-runtime branch moves to a single multi-tenant deployment. Key differences:
| Aspect | main (current) | spike/cf-runtime (target) |
|---|---|---|
| Runtime | Node.js (Astro @astrojs/node) | Cloudflare Workers (Astro @astrojs/cloudflare) |
| Database Driver | pg (Node.js pool) | @neondatabase/serverless (Neon HTTP) |
| Database Host | PostgreSQL on VPS | Neon (serverless Postgres) |
| CRM | Directus 11 (separate Docker container) | Built-in CRM (Astro pages) |
| File Storage | Directus-managed uploads | Cloudflare R2 with presigned URLs |
| Per-Client Infra | Pages project + DB + Directus + Tunnel + Access | Project = DB rows + Custom Hostname |
| Deployment | Dokploy + shell scripts | Cloudflare Pages + Dokploy (worker stack on CPX22) |
| Heavy Processing | Directus instance per client (floors included) | CPX22 worker box (OpenImmo, OCR, PDF gen, cron) |
Auth Boundaries
Section titled “Auth Boundaries”Three distinct authentication mechanisms serve different user types:
- Buyer auth — Passwordless OTP via email (Cloudflare Email Service) or WhatsApp (Meta Cloud API), backed by Lucia sessions
- CRM auth — Passwordless 6-digit OTP via email (magic-link deprecated since 2026-05), backed by a separate Lucia instance (
admin_sessions). Login page at/(root). - Operator auth — Cloudflare Access (Zero Trust) JWT verification for Convertella staff
Tenant Context
Section titled “Tenant Context”Every HTTP request resolves a TenantContext object that carries projectId, organizationId, portalId, and isOperator flag. This context scopes all database queries to the correct tenant.
Shared Zod Schemas
Section titled “Shared Zod Schemas”src/schemas/ defines Zod schemas for every API response and request body. The same schemas validate API responses server-side and parse client-side fetches via the typed API client.
import { UnitListResponseSchema } from 'src/schemas/unit';const data = await api.get(UnitListResponseSchema, '/api/admin/units');// Note: API routes use /api/admin/* prefix; browser pages use clean pathsTyped API Client
Section titled “Typed API Client”src/lib/api-client.ts provides api.get/post/patch/put/delete helpers that:
- Accept a Zod schema for response validation
- Raise
ValidationErroron schema mismatch - Raise
ApiErroron non-2xx HTTP responses - Work in both SSR (Astro frontmatter) and client (Preact islands) contexts
Document Storage
Section titled “Document Storage”Floor plans, exposes, energy certificates, brochures, and other PDFs are stored in Cloudflare R2 buckets. Admin uploads use presigned URLs (browser uploads directly to R2, bypassing the server). Document downloads are gated behind authentication and tracked as lead activity.