Skip to content

Key Concepts

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.

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.

The main branch uses per-client infrastructure. The spike/cf-runtime branch moves to a single multi-tenant deployment. Key differences:

Aspectmain (current)spike/cf-runtime (target)
RuntimeNode.js (Astro @astrojs/node)Cloudflare Workers (Astro @astrojs/cloudflare)
Database Driverpg (Node.js pool)@neondatabase/serverless (Neon HTTP)
Database HostPostgreSQL on VPSNeon (serverless Postgres)
CRMDirectus 11 (separate Docker container)Built-in CRM (Astro pages)
File StorageDirectus-managed uploadsCloudflare R2 with presigned URLs
Per-Client InfraPages project + DB + Directus + Tunnel + AccessProject = DB rows + Custom Hostname
DeploymentDokploy + shell scriptsCloudflare Pages + Dokploy (worker stack on CPX22)
Heavy ProcessingDirectus instance per client (floors included)CPX22 worker box (OpenImmo, OCR, PDF gen, cron)

Three distinct authentication mechanisms serve different user types:

  1. Buyer auth — Passwordless OTP via email (Cloudflare Email Service) or WhatsApp (Meta Cloud API), backed by Lucia sessions
  2. 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).
  3. Operator auth — Cloudflare Access (Zero Trust) JWT verification for Convertella staff

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.

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 paths

src/lib/api-client.ts provides api.get/post/patch/put/delete helpers that:

  • Accept a Zod schema for response validation
  • Raise ValidationError on schema mismatch
  • Raise ApiError on non-2xx HTTP responses
  • Work in both SSR (Astro frontmatter) and client (Preact islands) contexts

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.