K
Koltrix docs

Architecture

Koltrix is a small, opinionated set of services running behind nginx. Everything is multi-tenant; tenants are isolated at the PostgreSQL schema level so cross- tenant data leaks are impossible by construction.

Service map

                     ┌─────────────────┐
                     │  Cloudflare     │  *.koltrix.com → origin
                     └────────┬────────┘

                       ┌──────▼──────┐
                       │   nginx     │  TLS termination, host routing
                       └──┬──────┬───┘
              ┌───────────┘      └─────────────┐
              │                                │
   app.koltrix.com                   api.koltrix.com / docs.koltrix.com
              │                                │
   ┌──────────▼──────────┐        ┌────────────▼────────────┐
   │  koltrix-web        │        │  koltrix-api (Fiber)    │
   │  (Next.js 14 SSR)   │        │  /api/v1 + /api/v2      │
   └──────────┬──────────┘        └──┬──────────────┬───────┘
              │                      │              │
              └──────► REST ────────►│              │
                                     ▼              ▼
                              ┌───────────┐  ┌──────────────┐
                              │  Asynq    │  │   Postfix +  │
                              │  worker   │  │   Dovecot    │
                              └─────┬─────┘  └──────┬───────┘
                                    │               │
                          ┌─────────┴───┐    ┌──────┴──────┐
                          │ koltrix-smtp│    │  Rspamd     │
                          │ (relay 2525)│    │  (DKIM)     │
                          └─────────────┘    └─────────────┘
 
                Storage: Postgres 16 · Redis 7 · MinIO · Meilisearch

Containers

ServicePurpose
koltrix-webNext.js front-end (app, dashboard, settings)
koltrix-apiGo HTTP + WebSocket API (Fiber)
koltrix-workerBackground processor (campaigns, AI, webhooks)
koltrix-smtpESMTP relay on port 2525 (AUTH PLAIN with kx_ keys)
koltrix-docsDocumentation site (this app)
postfixInbound MX + outbound SMTP (port 25/587/465)
dovecotIMAP for human mailboxes
rspamdSpam filter + DKIM signing
postgresSource of truth, schema-per-tenant
redisAsynq queue, sliding-window rate limits, idempotency cache
minioObject storage (HTML bodies, attachments)
meilisearchFull-text email search

Tenancy model

  • public.tenants(id, slug, clerk_org_id, ...) — one row per organisation.
  • A per-tenant Postgres schema named org_<sanitized_slug> holds all business data (threads, emails, lists, subscribers, campaigns, sequences, templates, …).
  • A trigger on public.tenants runs ensure_*_objects(schema) so new tenants are provisioned with the full set of tables automatically.
  • The auth middleware extracts the tenant from the Clerk JWT (org_id or org_slug) and sets search_path for every request.

Request lifecycle

nginx → recover → logger → CORS → IP rate-limit → Clerk JWT
      → tenant middleware (SET search_path)
      → per-user rate-limit
      → handler

For /api/v2/* the Clerk middleware is skipped and an API-key middleware authenticates instead, resolving the tenant from api_keys.tenant_id.

Outbound message lifecycle

  1. Caller hits POST /api/v2/emails (or AUTH+DATA on SMTP relay).
  2. Handler enqueues TaskSendEmail and writes an outbound_messages row with status queued.
  3. Worker checks the suppression list for each recipient. Suppressed? skipped silently.
  4. Worker checks the warmup quota for the sender domain. Exceeded? the message is paused.
  5. Worker hands the MIME to Postfix on port 587. Rspamd DKIM-signs it.
  6. On success → status sent + message.sent webhook fires. On 5xx → status bounced, recipient auto-added to suppression list, message.bounced webhook fires.
  7. Recipients that open hit /t/o/:tokenoutbound_messages.opened_at updated + message.opened webhook fires. Same for clicks via /t/c/:token.