---
title: Architecture
path: concepts/architecture
status: published
---

# 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** — `arq` over 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

```mermaid
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 marked `active`.
- **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 `users` table (synced via `services/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](./registry) and event protocols.

## Data flow at a glance

```mermaid
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](./multi-tenancy) — how the partner/tenant/user model works in practice.
- [Subscriptions](./subscriptions) — state machine + lifecycle endpoints.
- [Authentication & RBAC](./authentication-and-rbac) — scopes, roles, and how the JWT becomes a permission set.
