Deployment
Audience: operators running Aperture as a long-lived service.
Aperture ships as a single pure-Go binary (CGO_ENABLED=0, no external runtime).
Running it as a service is one command — aperture serve — which hand-wires the
whole dependency graph (storage → engine → service → HTTP handler) and boots a
net/http server exposing the HTTP + Twirp API and the admin UI.
bin/aperture serve --addr :8080
aperture serving on :8080
serve listens on :8080 by default and shuts down gracefully on SIGINT /
SIGTERM, draining in-flight requests within a 10-second window before it
forces the listener closed.
Flags
| Flag | Default | Env source | Purpose |
|---|---|---|---|
--addr | :8080 | — | TCP address to listen on. |
--store | (in-memory) | — | SQLite DSN for the backing store. Empty ⇒ ephemeral in-memory store. |
--seed | (embedded example) | — | Path to a JSON/YAML seed model. Empty ⇒ the embedded acme example fixture. |
--auth | dev | APERTURE_AUTH_MODE | Authenticator adapter: dev, oidc, or parsec. The flag overrides the env var. |
--enforce-membership | off | APERTURE_ENFORCE_MEMBERSHIP | Deny any decision whose principal is not a member of the active account, before grants are consulted. |
The generated, always-current flag table is the Command-Line Reference.
The backing store (--store)
--store selects the storage backend:
-
Empty (the default) → the pure-Go in-memory backend. Ideal for demos, CI, and read-only trials; nothing is persisted across restarts.
-
A DSN → the SQLite backend (
modernc.org/sqlite, pure-Go, so CGO stays off). The DSN is amodernc.org/sqlitedata source name — a file path or afile:URL with pragmas, for example:bin/aperture serve --store 'file:aperture.db?_pragma=busy_timeout(5000)'
The SQLite pool is capped at a single connection so writes serialize cleanly
under SQLite’s single-writer model. On startup the server runs the embedded
schema (Setup) and then loads the model from --seed (or the embedded
example) into the store.
Configuration precedence
Aperture is configured, in order of increasing precedence:
.envfile — when you launch throughmake, a.envin the working directory is loaded and its keys are exported into the environment before the binary runs (include .env; exportin theMakefile). This is the dotenv convenience; the binary itself simply reads its process environment.APERTURE_*environment variables — the primary configuration surface. The authenticator is built from these viaauth.ConfigFromEnv(see below).- Command-line flags — a flag that declares an env source overrides that
env var. For example
--auth oidcwins overAPERTURE_AUTH_MODE=dev.
The seed model itself is authored as YAML (or JSON) and supplied with
--seed; that document also carries the providers: wiring the server turns
into a live object-provider registry. Model YAML is data, not process config —
the two are separate.
Authentication environment variables
The authenticator adapter maps each request to an Aperture principal. The
default is dev (the bearer token is the principal id), so serve runs with
no external identity provider out of the box; oidc and parsec are opt-in.
| Variable | Applies to | Meaning |
|---|---|---|
APERTURE_AUTH_MODE | all | Adapter: dev | oidc | parsec. Empty ⇒ dev. |
APERTURE_AUTH_PRINCIPAL_CLAIM | oidc, parsec | Verified claim mapped to the principal id (sub, email, …). Empty ⇒ sub. The dev adapter ignores it. |
APERTURE_OIDC_ISSUER | oidc | OIDC issuer URL. |
APERTURE_OIDC_AUDIENCE | oidc | Expected token audience. |
APERTURE_OIDC_JWKS_URL | oidc | JWKS endpoint for signature verification. |
APERTURE_PARSEC_KEYRING | parsec | Path to the broker’s persisted signing keyring (keyring.json) Aperture verifies brokered tokens against. |
APERTURE_PARSEC_STATE_DIR | parsec | Parsec broker state directory. |
An unrecognised APERTURE_AUTH_MODE fails the boot with APERTURE_CONFIG_INVALID.
The oidc adapter performs network discovery at startup, so misconfiguration
there surfaces immediately when serve boots.
The enforce-membership toggle (--enforce-membership /
APERTURE_ENFORCE_MEMBERSHIP) is defence-in-depth: a non-member of the active
account is denied before any grant is read, which is what lets a single shared
role (manager, analyst, …) be reused across customer accounts without one
account’s grants leaking to another’s members.
Manual dependency injection
serve builds its graph with plain constructors — no DI framework (no
wire/fx/dig). Each layer is a hand-written call:
buildStore(--store, --seed) # storage backend + schema + seed
→ engine.New(store, …) # decision engine (+ scope resolution, membership)
→ service.New(eng, …) # the fully-wired facade
→ server.New(svc) # HTTP + Twirp handlers + admin UI
→ server.Authenticate(authn, …) # request → principal middleware
The same fully-wired facade the mutation CLI commands build is what serve puts
behind a listener — the engine for decisions, the admin gate for tier checks, the
delegation and impersonation services, the append-only audit trail (decisions
sampled at 100 % under serve so the demo trail is legible), the rules engine
over a storage-backed rule source, and the seed’s declared object providers. A
rule saved through the admin UI takes effect on the next decision with no
separate rule store.
Because the wiring is explicit Go, there is no configuration container to learn:
the constructor order in internal/cli/serve.go is the deployment topology.
Related
- serve — the command page, with the flag walkthrough.
- Performance & NFR — the decision hot-path budget and how to assert it.
- Troubleshooting — reading and acting on
APERTURE_*boot/runtime errors. - Command-Line Reference — the generated flag table.