# Deployment architecture

Crown ships as a Docker Compose-based system with three repo-defined runtime shapes, plus several curated production reference patterns for Ubuntu / Coolify deployments.

The preferred small-production API shape is now
`docker-compose.coolify-core.yml`: one `crown-core-api` container hosts
Auth, Forms, Person, User Activity, CMS, Audit, Support, and Analytics while
keeping their existing hostnames. The legacy split-service Compose shape is
retained for rollback and for modules that later need independent scaling.
Frontend apps, NELA, risk scoring, AI, and ML training are not folded into the
core runtime.

Those curated references now explicitly cover a more realistic delivery plane too: **self-hosted Forgejo** as the Git provider and **self-hosted OCI registry** flows where source, build, and deploy are kept inside your own infrastructure.

- **development** — full local stack with Traefik and exposed ports
- **production** — internal container networking with domain routing
- **E2E testing** — isolated test stack with in-memory database behaviour
- **single-server Coolify** — one Ubuntu host running Crown, Coolify, and a single-node SurrealDB runtime
- **split build/runtime on Ubuntu + Coolify** — one runtime host managed by Coolify, with images built on a dedicated Coolify build server and pushed through a registry
- **multi-node Coolify + SurrealDB cluster** — multiple workload servers using per-server Coolify proxies and domain splits, plus a TiKV-backed SurrealDB data tier

## Deployment topology

![Deployment topology](../diagrams/generated/service-topology.svg)

Source: [`service-topology.mmd`](../diagrams/generated/service-topology.mmd)

## Build and deploy order

![Build and deploy order](../diagrams/generated/deployment-order.svg)

Source: [`deployment-order.mmd`](../diagrams/generated/deployment-order.mmd)

## Single-server Coolify reference

This is the smallest production-shaped deployment that still keeps Crown coherent: one Ubuntu server, one Coolify installation, one proxy layer, self-hosted Forgejo and registry services, and one single-node SurrealDB runtime.

![Single-server Coolify deployment](../diagrams/generated/single-server-coolify-deployment.svg)

Source: [`single-server-coolify-deployment.mmd`](../diagrams/generated/single-server-coolify-deployment.mmd)

See [Single-server Coolify deployment](./single-server-coolify-deployment.md) for the operating model, when to use it, and when to move beyond it.

## Multi-node Coolify + SurrealDB reference

This is the scale-out reference for Crown when one machine is no longer enough: a self-hosted delivery plane feeds Coolify-managed workload hosts, each with its own proxy and domain set, while SurrealDB moves to its documented TiKV-backed cluster mode.

![Multi-node Coolify + SurrealDB deployment](../diagrams/generated/multi-node-coolify-surrealdb-deployment.svg)

Source: [`multi-node-coolify-surrealdb-deployment.mmd`](../diagrams/generated/multi-node-coolify-surrealdb-deployment.mmd)

See [Multi-node Coolify + SurrealDB deployment](./multi-node-coolify-surrealdb-deployment.md) for the per-server domain-routing model, control-plane split, data-tier notes, and scaling guidance.

## Ubuntu VPS / dedicated server with Coolify

When Crown is hosted on a single Ubuntu VPS or a larger dedicated Ubuntu server, a practical production pattern is:

- **Coolify runs on the target host** and manages the deployed services
- **Forgejo acts as the self-hosted Git provider**
- **a self-hosted registry stores private images**
- **a dedicated Coolify build server** performs the remote image build and pushes the result to the registry
- **the Ubuntu host pulls prebuilt images** rather than compiling on the production box

![Ubuntu / Coolify hosted deployment](../diagrams/generated/ubuntu-coolify-deployment.svg)

Source: [`ubuntu-coolify-deployment.mmd`](../diagrams/generated/ubuntu-coolify-deployment.mmd)

See [Ubuntu / Coolify hosted deployment](./ubuntu-coolify-deployment.md) for the full reference flow, host responsibilities, and rollout sequence.

## Public docs site container

The docs themselves can now be published independently as a small Bun-served container.

Use [Publishing the docs site](../guides/publishing-docs.md) together with `docker/Dockerfile.docs` when you want a public architecture portal separate from the main Crown runtime.

## Main runtime containers

The table below lists logical runtime responsibilities. In the combined
Coolify shape, the eight ordinary Bun APIs share the `crown-core-api`
container and port `8080`; in the legacy split shape they retain the
individual containers and ports shown below.

| Runtime                    | Role                    | Default port / mapping | Notes                                                        |
| -------------------------- | ----------------------- | ---------------------- | ------------------------------------------------------------ |
| `traefik`                  | Reverse proxy           | `80`, `8080`           | Optional in dev profile, primary routed entrypoint           |
| `surrealdb`                | Database                | `8010:8000`            | Main operational store                                       |
| `database-migrations`      | One-shot job            | n/a                    | Applies SurrealQL migrations before dependent services start |
| `auth-service`             | Auth / session / JWKS   | `3000`                 | Central auth provider                                        |
| `records-service`          | Forms API               | `3095`                 | CRUD, stats, locks, exports                                  |
| `entity-service`           | Person API              | `3003`                 | Person records, live stats, matching                         |
| `audit-service`            | Audit / FGAC            | `5510`                 | Access logging and audit APIs                                |
| `analytics-service`        | Analytics API           | `3006`                 | Reports, snapshots, live dashboards                          |
| `user-activity-service`    | Activity API            | `3097`                 | Activity feeds and user-facing stats                         |
| `cms-service`              | Content API             | `3098`                 | Announcements, nav links, pages                              |
| `support-service`          | Support / notifications | `3099`                 | Tickets, messages, notifications                             |
| `nela-service`             | Lookup service          | `3018`                 | NELA clinician lookup                                        |
| `risk-score-service`       | Risk scoring            | `8000`                 | R/plumber-based API                                          |
| `auth-ui-app`              | Frontend                | `5175:80`              | Authentication UI                                            |
| `audits-app`               | Frontend                | `5173:80`              | Main audit UI                                                |
| `application-list-app`     | Frontend                | `5174:80`              | App selection and navigation                                 |
| `schema-designer-app`      | Frontend                | `5176:80`              | Schema design tooling                                        |
| `admin-ui-app`             | Frontend                | `5177:80`              | Admin and operational tooling                                |
| `nela-risk-calculator-app` | Frontend                | `5178:80`              | Risk calculator UI                                           |
| `analytics-ui-app`         | Frontend                | `5180:80`              | Analytics dashboards                                         |

## Environment shapes

### Development

Development uses the main `docker-compose.yml` file:

- ports are exposed directly on localhost
- Traefik can route `.localhost` domains to services and frontends
- frontend apps are built with host-accessible URLs
- backend services still use container-internal DNS names for service-to-service calls

### Production

Production uses `docker-compose.prod.yml` as the runtime-oriented override:

- SurrealDB stays internal to the network
- services use container DNS names such as `http://auth-service:3000`
- frontends are built with public external URLs
- Traefik handles routed HTTPS access
- persistent volumes remain attached for SurrealDB and support attachments

### E2E

E2E uses `docker-compose.e2e.yml` as an override:

- SurrealDB switches to in-memory storage for clean test runs
- frontend ports are pinned to expected Playwright values
- build args are overridden so in-stack URLs work correctly during tests
- an optional `e2e-tests` container can execute Playwright inside Docker

## Routing model

`docker/nginx.conf.template` shows the frontend deployment pattern:

- SPAs are served from nginx
- all unknown routes fall back to `index.html`
- assets are aggressively cached

This means the frontend containers are static web deployments, while the API logic remains in the Hono services.

## Volumes and state

Persistent state is currently concentrated in two places:

- `surrealdb-data` — primary application data
- `support-attachments` — uploaded support attachments

That makes operational backup planning fairly clean: protect the database volume first, then the support attachment volume.

## Deployment maintenance checklist

When changing the deployment architecture, update at least these areas:

- compose files (`docker-compose*.yml`)
- Dockerfiles for affected apps/services
- `docker/Dockerfile.docs` if the public docs container behaviour changes
- rsbuild env mappings for affected frontends
- `.env.example` / environment references where relevant
- regenerate docs from `apps/docs-site/` via `bun run generate`
