Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Deployment

Running ScaiSend in production. Three services, three datastores, and a handful of DNS records. This page covers the moving parts; see the standalone deployment guide shipped with the repository (docs/deployment-standalone.md) for copy-pasteable install steps.

What you're running#

Service Command Role
API scaisend serve FastAPI; handles /v3/*, /api/admin/*, /t/*, /i/*, /webhooks/scaikey
Worker scaisend worker arq; renders templates, delivers webhooks, aggregates stats
SMTP scaisend smtp aiosmtpd; direct outbound delivery + inbound DSN/FBL

All three can run on a single host for small deployments, or split across hosts for scale. They're stateless — all state is in MySQL, Redis, or S3 — so you can add replicas freely.

Dependencies#

System Purpose Version
Python Runtime 3.11+
MySQL Durable state (27 tables) 8.0+
Redis Queues, cache, rate-limit counters 7+
S3-compatible Attachments, images Garage, MinIO, or real S3
ScaiKey Identity, OAuth, webhook-based sync Accessible over HTTPS

A production deployment also needs:

  • Public HTTPS for the API. Typically via a reverse proxy (Nginx, Caddy, Traefik) terminating TLS.
  • Public IP on port 25 for the SMTP inbound server (DSN and FBL reception). Reachable from the internet.
  • Outbound port 25 unrestricted (or restricted to your SMTP service's egress IPs). Many cloud providers block outbound 25 by default; contact support to unblock.
  • Publishing DNS for each sender domain (DKIM, SPF, DMARC) and for the ScaiSend instance itself (A/AAAA, reverse DNS).

Configuration#

All configuration is via environment variables. Minimal set:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Database
DATABASE_URL=mysql+aiomysql://scaisend:password@mysql/scaisend

# Redis
REDIS_URL=redis://redis:6379

# S3
S3_ENDPOINT_URL=https://s3.example.com
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=scaisend-attachments

# ScaiKey
SCAIKEY_API_URL=https://scaikey.scailabs.ai
SCAIKEY_CLIENT_ID=...
SCAIKEY_CLIENT_SECRET=...
SCAIKEY_WEBHOOK_SECRET=...
SCAIKEY_JWKS_URL=https://scaikey.scailabs.ai/.well-known/jwks.json

# SMTP
SMTP_HELO_HOSTNAME=mail.example.com
SMTP_POOL_SIZE_PER_DOMAIN=5
SMTP_CONNECT_TIMEOUT=30
SMTP_COMMAND_TIMEOUT=60
SMTP_MAX_RETRIES=5
SMTP_RETRY_BASE_DELAY=60
INBOUND_SMTP_HOST=0.0.0.0
INBOUND_SMTP_PORT=25

# Tracking
TRACKING_BASE_URL=https://scaisend.example.com

# Admin UI
ADMIN_URL=https://scaisend.example.com

See .env.example in the repository for every variable.

Self-registration with ScaiKey#

If your ScaiKey deployment allows self-registration, let ScaiSend bootstrap itself:

bash
1
2
3
scaisend register \
  --public-url https://scaisend.example.com \
  --contact-email admin@example.com

This:

  1. Calls ScaiKey's /v1/clients/register with ScaiSend's metadata.
  2. Waits for an admin to approve the registration (or auto-approves if ScaiKey is configured for that).
  3. Writes the returned client credentials to .env.

Check status with:

bash
1
scaisend register-status --update-env

For scoped registration (one tenant or partner only):

bash
1
2
scaisend register --public-url https://... --tenant acme-corp --contact-email ...
scaisend register --public-url https://... --partner reseller-xyz --contact-email ...

Initial sync#

After registration, mirror the current state from ScaiKey into your ScaiSend DB:

bash
1
scaisend sync --full --create-roles

--create-roles creates the default admin, developer, and viewer roles for every tenant. --full mirrors every partner, tenant, user, and group.

Re-run after prolonged ScaiKey outages, but it's unnecessary during normal operation — webhooks keep everything in sync in real time.

Database migrations#

Alembic handles schema. On first install:

bash
1
alembic upgrade head

On upgrade:

bash
1
2
3
4
5
# Back up first
mysqldump scaisend > scaisend-$(date +%Y%m%d).sql

# Then migrate
alembic upgrade head

Migrations are forward-only. There's no automated downgrade path; if you need to roll back, restore from backup.

Running the services#

Systemd units#

A typical setup runs each service as a separate systemd unit:

systemd
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# /etc/systemd/system/scaisend-api.service
[Unit]
Description=ScaiSend API
After=network.target mysql.service redis.service

[Service]
User=scaisend
EnvironmentFile=/etc/scaisend/.env
ExecStart=/opt/scaisend/.venv/bin/scaisend serve --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target

Replicate for scaisend-worker and scaisend-smtp. Put the API behind a reverse proxy terminating TLS on 443.

Docker / Kubernetes#

ScaiSend runs cleanly in containers. Run three separate deployments (one per service), each with its own image tag or process flag. Scale independently.

Key things to remember:

  • The SMTP service needs hostNetwork: true (Kubernetes) or network_mode: host (Docker Compose) if it's listening on port 25 — running it in a NAT-ed container means recipient MXs see a private IP in the reverse DNS check.
  • All three services share the .env — the same MySQL, Redis, S3 credentials.
  • Only the API service needs inbound HTTP exposure.

DNS for the ScaiSend instance itself#

Separate from per-sender-domain DNS, the ScaiSend instance hosts some records:

Record Purpose
A / AAAA for the API hostname HTTPS traffic
A / AAAA for the SMTP inbound hostname DSN / FBL reception (can be same as API host)
PTR for the SMTP outbound IPs Reverse DNS; required by most receivers for deliverability
MX for the abuse address domain If you're receiving FBLs, the MX must point at the SMTP inbound server

PTR records are usually managed by your IP provider. Make sure every outbound IP has a PTR pointing at a hostname that resolves back to that IP (forward-confirmed reverse DNS, FCrDNS).

Health checks#

Endpoint Purpose
GET /health Liveness — returns 200 if the API process is up
GET /ready Readiness — returns 200 if DB is reachable, 503 otherwise

Point your load-balancer health checks at /ready. Kubernetes livenessProbe at /health; readinessProbe at /ready.

The worker and SMTP services don't expose HTTP; rely on systemd / Kubernetes to restart them if they die.

Backup and recovery#

State in order of criticality:

Store Backup strategy
MySQL Regular mysqldump or managed-DB snapshots. Daily minimum.
S3 Versioning + replication. Attachments aren't regenerable from anywhere else.
Redis No backup. Queued-but-unsent messages are lost on wipe; persistently stored messages survive.

On restore:

  1. Restore MySQL to the desired point in time.
  2. Restore S3 attachments (or rely on durability).
  3. Let Redis rebuild naturally. Messages in the QUEUED state in MySQL will be picked up by the worker on next poll.

Scaling#

Bottleneck How to scale
API throughput Add more API replicas behind the load balancer
Template rendering Add more worker replicas
SMTP delivery concurrency Increase SMTP_POOL_SIZE_PER_DOMAIN; add more SMTP replicas
Inbound DSN/FBL volume One SMTP replica is usually plenty; only add if you're processing millions of DSNs/day
MySQL write load Vertical scale MySQL; tune indexes; consider a read-replica for /v3/messages reads
Redis memory Vertical scale; queue depth is usually the bottleneck, not key count
Updated 2026-05-17 01:33:27 View source (.md) rev 1