Architecture
ScaiControl is the operator-grade control plane that ties the ScaiLabs service ecosystem together. It owns the billing and subscription relationships, maintains a registry of installed services, and exposes a single admin surface across them. This page describes the moving parts.
Top-level components#
- Backend — FastAPI + SQLAlchemy 2.0 async + MariaDB Galera. Single Python service exposing
/api/v1/*REST endpoints and an MCP server with 16 tools. - Frontend — SolidJS 1.9 + Tailwind 4 + DaisyUI 5. The customer-facing portal (catalog, billing, usage) and the admin UI both live in one bundle, gated by RBAC.
- Background worker —
arqover Redis. Owns provisioning loops, heartbeat monitoring, identity reconciliation, the event dispatcher, the subscription reaper, the trial-ending notifier. - Storage — MariaDB Galera for relational state, S3-compatible object store for invoice PDFs and uploaded assets, Redis for queues and short-lived state.
Tenancy hierarchy#
flowchart TB
P["<b>Platform</b><br/><i>operator</i>"]
PA["<b>Partner</b><br/><i>resells to its own customers, or is the operator itself</i>"]
T["<b>Tenant</b><br/><i>the billable entity, has users</i>"]
U["<b>User</b><br/><i>authenticates via ScaiKey, scoped by role</i>"]
P --> PA --> T --> U
Every API call below /api/v1/* (except a handful of public ones) carries a JWT whose claims pin a partner_id and a tenant_id. Queries on tenant-scoped tables are auto-filtered server-side via a SQLAlchemy do_orm_execute event hook (db/tenant_filter.py), so leaks across tenants aren't possible at the ORM layer.
What ScaiControl owns#
- Service registry — every running service (ScaiKey, ScaiVault, ScaiFlow, …) registers here. Registration carries
app_id,base_url,health_check_url, capabilities. Heartbeats keep the row markedactive. - Plans + service packs — pricing units. Plans belong to a service; service packs bundle multiple
(service, plan)pairs under one billable line. - Subscriptions — the active link between a tenant and a (service, plan). Lifecycle is a state machine:
pending → trialing → active → past_due/cancelling → cancelled/expired/suspended. - Billing profiles — buyer-side identity for invoices (
tenant_billing_profiles) and the seller-side for partners (partner_configuration). - Invoices and credit notes — EU-compliant, with VAT determination, finalisation immutability, multi-format e-invoicing (UBL, XRechnung, ZUGFeRD, Factur-X), and a GrapesJS-based template designer with language variants.
- Outbound events + webhook subscribers — every billing or subscription transition emits a signed webhook; subscribers (CRM, accounting bridges, dashboards) are managed in the admin UI.
- Payment providers — Stripe, Mollie, and Coinbase Commerce (crypto) via a plugin architecture.
What ScaiControl does NOT own#
- Identity. ScaiKey owns users, groups, OIDC. ScaiControl consumes the JWT and reflects a denormalised copy of identity into its own
userstable (synced viaservices/identity_sync.py). - Secrets. ScaiVault owns API keys and webhook signing secrets. ScaiControl resolves them by vault path at use time.
- Email transport. ScaiSend ships the outgoing mails; ScaiControl renders the template + envelope and hands off to ScaiSend.
- The actual services being subscribed to — those are independent applications that integrate via the registry and event protocols.
Data flow at a glance#
flowchart LR
SK["ScaiKey"] -->|login| P["Portal"]
P -->|REST| BE["ScaiControl backend"]
BE --- DB[("MariaDB<br/>state")]
BE --- S3[("S3<br/>PDFs, assets")]
BE --- R[("Redis<br/>queues")]
BE --> W["arq worker"]
W --> ED["event dispatcher"]
W --> RP["subscription reaper"]
W --> HM["heartbeat monitor"]
W --> TN["trial-ending notifier"]
W --> PR["provisioning loops"]
W --> IR["identity reconciliation"]
ED -->|HMAC POST| SUBS["external subscribers"]
HM -->|GET /health| SVCS["registered services"]
Surfaces#
| Surface | Path / mechanism |
|---|---|
| Tenant portal | https://<host>/ — catalog, services, billing, invoices, usage |
| Admin portal | https://<host>/admin/... — billing, partners, tenants, subscriptions, packs, templates, webhook subscribers |
| REST API | https://<host>/api/v1/... — same endpoints both surfaces use |
| MCP server | manage_subscriptions and 15 sibling tools for AI agents |
| CLI | scaicontrol admin ... — operator commands, runs in-process against the DB |
| Background jobs | arq scaicontrol.workers.main.WorkerSettings — cron + queue |
Where to look next#
- Multi-tenancy — how the partner/tenant/user model works in practice.
- Subscriptions — state machine + lifecycle endpoints.
- Authentication & RBAC — scopes, roles, and how the JWT becomes a permission set.