Run it in production
One image, one binary, --role web|worker|all. The reference repository ships
the image; the private clients repository ships the chart and the cluster state
that deploys it. Read
RELEASE.md for the reviewed tag and image workflow
and the clients repository’s deploy/README.md for the chart.
Configuration
config.example.yaml is the whole
configuration surface; a key nothing reads does not belong in it. The blocks
are server (address, public host, whether to serve the OpenAPI document, the
read timeout), database (an application URL as a role subject to row-level
security, and a migration URL as the owner), nats, log, auth.oidc,
mail, audit.retention_days and files. Every key can be overridden from the
environment as PLATFORMKIT_<SECTION>_<KEY>; secrets — the OIDC client secret,
the mail password — belong there and not in the file.
Roles
--role web serves HTTP. --role worker runs the outbox relay, the event
subscriptions and the periodic jobs. --role all does both and is what a
laptop runs; the boot flow shows what
each role starts. Every role applies pending migrations at start behind an advisory
lock, so there is no migration job and no --migrate-only; the deployment shape
that follows is a startup probe long enough for the ledger to apply, maxSurge
1 and maxUnavailable 0. Two worker replicas share one JetStream consumer
through a deliver group, so scaling out does not duplicate handling.
The image
deploy/Dockerfile builds apps/platformkit and runs it on a distroless base
as an unprivileged user. make image builds it locally. The release workflow
builds and publishes ghcr.io/septagon-oss/platformkit on a v* tag, with an
SBOM, and moves latest only for a non-prerelease.
Probes and headers
GET /health and GET /ready answer on the plain router, before tenant
resolution, so a liveness probe does not depend on a database round trip.
Every response carries X-Frame-Options: DENY, Referrer-Policy and
X-Content-Type-Options: nosniff; every HTML response carries a content
security policy with a per-request nonce, base-uri 'none' and
form-action 'self'; HSTS is set when the public host is not local;
authenticated HTML and private downloads are Cache-Control: no-store.
Uploaded files are served as attachments unless their stored type is in a
closed render-safe set, and never inline for HTML, SVG or any XML dialect.
Tenancy
The host of a request selects the tenant. The application connects as a role
with no superuser and no BYPASSRLS, every tenant table carries FORCE ROW
LEVEL SECURITY, and the tenant is set per transaction, so a forgotten WHERE
tenant_id returns nothing rather than another tenant’s rows. Cross-tenant work
— the tenant registry, hourly sweeps — runs under a db.Tx[db.System] that only
the composition can hand out, so grep SystemToken in apps/ lists every place
the boundary is crossed on purpose. The operator’s own tenant is the one the
bootstrap creates; operator permissions are refused at every other tenant
before the authorizer is asked.
Migrations
Each capability owns its SQL under migrations/<owner>/ as
<version>_<name>.up.sql, numbered within the owner. One runner applies the
foundation and each selected module’s history in composition order, tracking
every applied file by owner and revision, committing each file with its history
row in one transaction. A failed file rolls back and is retried on the next
start; applied files are immutable. There are no down files. The design is
ADR 0011; the clients repository’s operations notes
describe recovery from a partially applied file on the older ledger.