K
Koltrix docs

Database schema

Postgres 16, schema-per-tenant.

Public schema

Cross-tenant tables. Every business table references public.tenants(id).

TablePurpose
tenantsOrganisations + onboarding_state
tenant_domainsVerified sending domains
tenant_usersClerk users ↔ tenants
email_addressesHosted mailboxes ↔ users
api_keysHashed kx_… keys with permission scopes
suppression_listPer-tenant blocked recipients
outbound_messagesEvery send (queued → sent → delivered → …)
tracking_tokensOpen/click token → message_id mapping
webhooks / webhook_deliveriesEndpoints + delivery attempts
newsletter_unsubscribesToken → tenant/list/subscriber routing
newsletter_confirm_tokensDouble opt-in tokens
domain_warmupPer-domain daily send budget
activity_logAudit log
email_tracking_eventsLegacy per-event log
audit_logsCross-tenant audit log
analytics_eventsGeneric event store

Tenant schema (org_<slug>)

Provisioned automatically by a trigger on public.tenants. Tables:

TablePurpose
threadsEmail threads
emailsIndividual email records
attachmentsFile attachments (MinIO pointers)
email_readsRead receipts
ai_suggestionsAI-generated reply candidates
labels, foldersUser-defined organisation
email_forwardsForwarding rules
auto_repliesVacation / auto-reply
email_filtersInbox rules
ticketsHelpdesk
contacts, deals, companiesCRM
campaignsNewsletter broadcasts
calendar_eventsCalendar
filesKoltrix Drive
newsletter_listsAudiences
newsletter_subscribersPeople on lists
subscriber_segmentsSmart filters
email_templatesReusable HTML
inbox_viewsSaved per-user filters
sequences, sequence_stepsDrip automations
aliasesEmail aliases

Migrations

SQL files in koltrix-api/migrations/, applied automatically on API startup. The migrator records applied files in public.schema_migrations.

ID strategy

UUIDs everywhere (uuid_generate_v4() / gen_random_uuid()). No autoincrement columns. Tokens for tracking, unsubscribes, and confirms are 32 hex chars (crypto/rand).

Multi-tenant safety

Every request goes through tenant middleware that runs SET search_path TO org_<slug>, public on a connection that's then reused for the request lifetime. There's no way to query another tenant's data short of explicitly writing public.tenants joins.