Zettleinby Happy Sandboy
Menu

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