05 · Data
One schema, many jurisdictions.
Multi-tenant by country from day one. Jurisdiction is a property of the tenant, not a fork of the schema. Every meaningful state change writes an audit event.
Principles
Design constraints that never bend.
Tenant-scoped
Global row filter
Every user-owned row carries `tenantId`; per-service query filters and Postgres RLS as belt-and-suspenders.
Money is bigint cents
No floats
Amounts are stored as `bigint` cents with an explicit ISO currency. FX rates stored on the transaction, not implied.
Documents are artifacts
Immutable
Contracts and generated docs are versioned immutable PDFs plus their source DSL, e-sign proofs, and IPFS-style hash.
Event-sourced audit
AuditLog + events
State changes emit domain events and an AuditLog row. Regulators, disputes and AI training all draw from the same log.
AI is a first-class actor
AiAction table
Every model call, tool use, cost and latency is stored — never a black box.
Soft delete + retention
GDPR-ready
User data has `deletedAt`; PII fields are erasable while keeping transaction integrity.
Schema
Core entities (Prisma).
// Prisma sketch — trimmed for clarity
model Tenant { id String @id country String createdAt DateTime @default(now()) }
model User { id String @id tenantId String email String phone String?
role Role kyc KycProfile? createdAt DateTime @default(now()) }
model KycProfile { id String @id userId String @unique idDocUrl String
selfieUrl String status KycStatus verifiedAt DateTime? }
model Property { id String @id tenantId String ownerId String
category PropertyCategory titleType TitleType
address Json geo Geography status PropertyStatus
attrs Json createdAt DateTime @default(now())
media MediaAsset[] floorPlans FloorPlan[] listing Listing? }
model MediaAsset { id String @id propertyId String kind MediaKind
url String qaScore Float meta Json }
model FloorPlan { id String @id propertyId String svgUrl String rooms Json }
model Listing { id String @id propertyId String @unique
priceCents BigInt currency String descriptions Json // {km, en, zh}
marketing Json publishedAt DateTime? status ListingStatus }
model Offer { id String @id listingId String buyerId String
priceCents BigInt terms Json status OfferStatus }
model Contract { id String @id offerId String templateId String
version Int pdfUrl String signatures Signature[]
state ContractState }
model DocumentTemplate { id String @id jurisdiction String kind DocKind
dsl Json version Int }
model EscrowAccount { id String @id offerId String @unique provider String
balanceCents BigInt currency String }
model Transaction { id String @id escrowId String kind TxKind
amountCents BigInt fxRate Decimal? rail String
externalRef String? status TxStatus }
model ServiceProvider { id String @id tenantId String category ProviderCategory
coverage Json rating Float verifiedAt DateTime? }
model ServiceOrder { id String @id providerId String propertyId String
stage TransactionStage price BigInt status OrderStatus }
model Lead { id String @id tenantId String source String
intent LeadIntent ownerId String? stage String }
model Conversation{ id String @id userId String channel Channel
mode ZettleinMode messages Message[] }
model AiAction { id String @id conversationId String tool String
input Json output Json latencyMs Int costCents Int }
model AuditLog { id String @id actorId String? entity String
entityId String action String diff Json at DateTime }Relationships
Transaction spine.
User ─┬─▶ Property ─┬─▶ MediaAsset
│ ├─▶ FloorPlan
│ └─▶ Listing ─▶ Offer ─▶ Contract ─▶ EscrowAccount ─▶ Transaction
│ │
│ └─▶ DocumentTemplate (by jurisdiction + kind)
│
└─▶ Conversation ─▶ Message
└─▶ AiAction (tool-use trail)
ServiceProvider ─▶ ServiceOrder ─▶ Property (stage-aware)
AuditLog ← every write on the boxes above