Designer
← Docs

Architecture

System overview, apps, packages, database schema, auth flow, and deployment.

Overview

Airestate is a multi-tenant SaaS for real estate agents. Agents use AgentOS (back-office) to manage listings, clients, and their public website. Each agent's website runs on a custom domain, showcasing their listings with search, maps, and SEO.

Apps

AgentOS apps/agentos → airestate.app

Agent back-office (SPA, no SEO)

  • Route groups: (dashboard), (explorer), (admin), (auth)
  • Dashboard routes: /listings, /clients, /inbox, /website, /analytics, /team, /settings, /docs
  • Listing CRUD: /listings/create, /listings/[id]/edit
  • Auth routes: /login, /signup, /join (invite acceptance)
  • Auth state managed via AuthProvider React Context
  • In-app documentation at /docs with sidebar navigation and search

Website apps/website → custom domains

Public listing website (ISR/SSG, SEO-critical)

  • One codebase serves all tenants via hostname-based routing
  • Themed per workspace via CSS variables from website_config
  • Routes: /, /listings, /listings/[slug]
  • ISR: revalidate=3600 on listings and detail pages

Designer apps/designer → localhost:3002

Design system gallery (tokens, components, views, tenants)

  • Browse all tokens with visual previews at /tokens
  • Component gallery with live previews at /components
  • Material recipes at /tokens/materials
  • Tenant branding preview at /tenants

Packages

PackagePurposeKey Exports
packages/dbDrizzle schema, tenant isolation, migrationsforWorkspace(), forAdmin(), forSystem(), schema
packages/uiShared React components for both appsPropertyCard, PropertyGallery, SearchFilters, ExplorerProvider, formatPrice
packages/authSupabase Auth configuration and helperscreateClient(), createServerComponentClient()
packages/permissionsRBAC engine + plan limitscan(), authorize(), enforcePlanLimit()
packages/typesShared TypeScript typesProperty, PropertyWithImages, PropertyFilters, WebsiteConfig
packages/tokensDesign token definitions, CSS generator, tenant overridesTOKEN_DEFINITIONS, MATERIALS, generateCSS()
packages/configShared ESLint, TS configsBase tsconfig, library tsconfig

Database

Provider: Supabase Postgres + PostGIS  | ORM: Drizzle  | Multi-tenancy: Discriminator column (workspace_id) with application-level enforcement

Schema Tables

workspaces
Tenants (plan, limits, status)
website_configs
Per-workspace website config
users
Workspace members
user_permissions
Toggle-based permissions
clients
Agent clients
invitations
Team invite tokens (7-day)
otp_codes
Passwordless OTP codes
properties
Real estate listings (ALS)
property_images
Images with order + primary
property_agents
Property-agent junction
buildings
Condo/dev buildings
tags
Tag registry (5 categories)
neighborhoods
Geographic areas + boundaries
videos
Property videos
page_views
Internal analytics
revalidation_queue
Debounced ISR queue

Property Data Model Highlights

  • Location: lat/lng (doublePrecision), PostGIS location (auto-populated via trigger)
  • Price: bigint stored in cents, priceCurrency (3-char ISO code)
  • Tags: 5 JSONB arrays (collections, features, amenities, facilities, lifestyles) with GIN indexes
  • Cover image: auto-synced via DB trigger from property_images.isPrimary
  • Classification: typeTerm (Sale/Rent), propertyType, propertySubType

Key Indexes

  • GIN indexes on all 5 JSONB tag arrays
  • Partial index on is_visible = true for public queries
  • Unique partial index on property_images(property_id) WHERE is_primary = true
  • Composite index on (workspace_id, status) and (workspace_id, slug)

Tenant Isolation

forWorkspace(workspaceId) → scoped queries (default for all app code)
forAdmin(role) → cross-tenant queries (superadmin only)
forSystem() → migrations, seeds, website data layer
Raw db client is never exported. Enforced by module structure and CI lint.

Auth

Provider: Supabase Auth (passwordless OTP)  | Email: Resend API

  • Agents/Owners: passwordless 6-digit OTP via email
  • Clients: magic link (email)
  • No passwords stored -- fully passwordless

OTP Flow

  1. User enters email → POST /api/auth/send-otp generates 6-digit code, stores in otp_codes table, sends via Resend
  2. User enters code → POST /api/auth/verify-otp validates code, calls admin.generateLink() → returns token_hash
  3. Client exchanges token_hash via supabase.auth.verifyOtp() → Supabase session cookie set
  4. Middleware checks Supabase session → redirects unauthenticated to /login
  5. API routes call getAuthUser() → AuthUser with id, email, workspaceId, role, permissions

OTP Security

  • 10-minute expiry, max 5 verification attempts per code
  • 60-second rate limit between OTP requests per email
  • crypto.randomInt() (CSPRNG) for code generation
  • Email enumeration prevention: send-otp always returns success

Permissions

Platform role: superadmin (bypasses all) or member (default). Workspace permissions are toggle-based per user.

CategoryPermissionDescription
Adminworkspace_masterFull access, at least 1 required
Adminmanage_membersInvite/edit/remove teammates
Adminplan_billingManage billing and plan
AdminanalyticsWorkspace-wide analytics
AgentexplorerView all properties + insights
Agentclients_fullAll clients, can reassign
Agentclients_restrictedOnly assigned clients
Agentlisting_managerCreate/edit/publish listings
Agentmarketing_hubPublish to social/website

Plan Limits

LimitFreeProBusiness
Properties101001,000
Users1525
Clients202002,000
Images505005,000
Custom DomainNoYesYes
AnalyticsNoYesYes

Key Decisions

DecisionChoiceWhy
Two apps, not oneAgentOS + WebsiteDifferent rendering models (SPA vs ISR)
DatabaseSupabaseIntegrated platform (DB + Auth + Storage)
ORMDrizzleEdge-compatible, transparent SQL
Multi-tenancyApp-level isolationExplicit, testable, debuggable
Image storageproperty_images tableExtensible (alt text, captions, sort order)
SearchPostgres search V150-500 listings/workspace, no external search needed
Map hover syncReact ContextScales to 15+ shared components without drilling
ISR strategyDebounced via queuePrevents storms with many tenants
Price storageCents (bigint)Avoids floating-point rounding in financial calcs
Tag storageJSONB arrays + GINFast reads, simpler queries for 5 tag types
Auth methodPasswordless OTPBetter security, no password resets, simpler UX
PermissionsToggle-basedGranular control, clear UI, no ambiguous roles

Deployment

  • Hosting: Vercel (auto-deploy on push to main)
  • AgentOS: myhello/airestate-agentos
  • Website: myhello/airestate-website with *.airestate.website wildcard domain
  • Custom Domains: Vercel Domains API -- agent adds domain in AgentOS, API adds to Website Vercel project, DNS verification, SSL auto-provisioned
  • File Storage: Supabase Storage (V1), images served via Supabase CDN
  • Maps: Mapbox GL JS (direct, not react-map-gl) with provider-agnostic adapter (supports Google Maps too)
  • CORS: Zero CORS by design -- all DB/search calls go through each app's own API routes