Getting Started
This page is the path a design partner walks the first week: get a toolchain,
install the CLI, prove the contracts are sound, run a service against local
Postgres, and put that service on your own infrastructure. Every command below
was run against the real cafe binary and the real service repositories, and the
output is quoted where it matters.
Status, first
Section titled “Status, first”cafaye is in early development. Of the thirteen cafe subcommands, eleven do
real work today — backup, contract lint/contract resolve, deploy, dev,
doctor, env, gen telemetry, lock, mcp, reclaim, version. The
remaining two (init, new) parse their flags, check their argument count,
and return not implemented in v0. That is stated on every step below rather
than hidden, because a command that silently half-works is worse than one that
refuses. cafe/internal/cli/stub_test.go holds the table this sentence is
derived from, and it is checked rather than remembered.
What is real: the contracts are frozen and validated, the
services have
HTTP surfaces you can call, container images you can build, a local stack you can
run, cafe dev will render and bring up a compose file from a manifest, and
cafe deploy will build, push and roll a service out. What
is not real: cafe gen, and the broker. The
runbooks assume you have already got the services running on your
own infrastructure.
The launch deploy path is the substrate, not Kamal. The customer-facing
deployment substrate — k3s + Traefik + cert-manager + ArgoCD + CloudNativePG +
Redis + Alloy/Loki/Grafana + Prometheus — is decided in
DEPLOYMENT.md in the moon repository (recorded publicly in
Deployment) and
is what the launch stack ships on. cafe deploy drives Kamal today; the k3s/Argo
implementation lands behind the same deploy.Runner interface, so the command
does not change — the infrastructure it drives does. Kamal retires at parity,
not before. Deployment is the runbook for the
substrate.
Step 1 — check the toolchain
Section titled “Step 1 — check the toolchain”cafe doctor is the first command to run, and it is safe anywhere. It resolves
binaries on PATH and never executes them: no subprocess, no container, no
network.
go install github.com/cafaye/cafe/cmd/cafe@latestcafe doctortool status found atgit ok /opt/homebrew/bin/gitdocker ok /usr/local/bin/dockerdocker compose ok /usr/local/bin/docker-composetilt ok /Users/kaka/.local/share/mise/shims/tiltgo ok /Users/kaka/.local/share/mise/shims/goruby ok /Users/kaka/.local/share/mise/shims/rubyelixir ok /Users/kaka/.local/share/mise/shims/elixirpython ok /Users/kaka/.local/share/mise/shims/python3bun ok /Users/kaka/.local/share/mise/shims/bunrust ok /Users/kaka/.cargo/bin/rustcchecked 10 tools, 10 ok, 0 missingIt checks ten tools, in that order: git, docker, docker compose, tilt,
go, ruby, elixir, python (looked up as python3 then python), bun,
rust (rustc then cargo). That list is the tools table in
cafe/internal/cli/doctor.go, one row each, in that order. doctor exits 1
when any row in the project or reclamation tables is fail — a warn never
moves it — so a red doctor is a real signal, not a decoration.
You do not need all ten. You need the languages of the services you intend to
run, plus git and docker. identity alone needs go; parlor needs node.
Run it with no argument and, having no project to plan, the project table still
prints — with a plan row that fails:
project .check state detail fixplan fail could not be planned: no manifest: no cafaye.yml in .; a project declares its service in one (run "cafe init" to create it, or pass the project directory as the argument) cafe contract lint .That is a failure — doctor exits 1. Point it at a project directory to get
the full project table, which is the one the tool table cannot answer — whether
the container runtime is answering, whether the machine has the memory and
CPUs a stack needs, whether the ports the stack publishes are free, and whether
the manifest is one that plans:
cafe doctor ../identityproject /path/to/identity: identity (go), 3 services in identity-devcheck state detail fixruntime ok server 29.4.0 -machine ok 16 GiB and 8 CPUs -ports ok every published port is free and inside 15000-15999 -plan ok 3 services in identity-dev -4 state check(s): 4 ok, 0 warn, 0 failFour rows there are invisible from the tool table. runtime asks the Docker
server, so an installed-but-stopped runtime reads fail while the table
above says ok. machine holds a floor of 4 GiB and 4 CPUs for a database, a
cache and a service at once. And the ports come from the same plan cafe dev
would build, so the two cannot drift. A third table, reclamation, follows:
whether the ledger holds stacks and port reservations nothing has reclaimed,
and whether anything outside cafe’s ledger is holding a port in 15000-15999.
-tools-only prints the first table alone.
Step 2 — install cafe
Section titled “Step 2 — install cafe”There is no tagged release, so @latest resolves to a pseudo-version built
from master. That works today:
go install github.com/cafaye/cafe/cmd/cafe@latestcafe versioncafe 0.0.0-dev (commit unknown)That version string is not a mistake. The semver and commit are injected at link
time by the release pipeline; a go install from an untagged commit has
neither, so the binary reports the development identity. When a release lands,
cafe version reports the release instead.
To build from a clone instead — which is what you want if you are going to read the source:
git clone git@github.com:cafaye/cafe.gitcd cafego build ./cmd/cafeThere is no Homebrew tap and no cafe install command. brew install cafaye/tap/cafe and cafe install are both
Coming soon; neither exists today, and neither
has a workaround beyond the two commands above.
Step 3 — read what the CLI can do
Section titled “Step 3 — read what the CLI can do”cafe helpcafe - the cafaye platform CLI
Usage: cafe <command> [flags] [arguments] cafe help <command>
Commands: backup take one real backup and prove it restores, into a scratch database it then drops contract validate cafaye manifests and resolve core version constraints deploy deploy a service with Kamal: build, push, roll out behind kamal-proxy dev run the local development stack for a project doctor report which cafaye toolchains this machine has, and whether it can run this project env provision the local stack for a command, and reclaim it when the command is done gen generate code and config from cafaye contracts init create a cafaye.yml manifest for the current project lock pin this tree's specs, vendored schemas and generated clients, and check the pins mcp serve the cafaye tools over the Model Context Protocol new scaffold a new cafaye service or app reclaim reclaim what a cafe session left behind: containers first, then volumes version print the cafe version help show help for a commandcafe help <command> is the same as <command> --help, and it is the authority
on flags — this page does not restate them, because a flag list copied into
prose is a flag list that drifts.
| Command | Takes | Works today |
|---|---|---|
cafe version |
— | yes |
cafe doctor |
0 or 1 | yes |
cafe contract lint <path> |
1 argument | yes |
cafe contract resolve <constraint> <version> |
2 arguments | yes |
cafe dev [project] |
0 or 1 | yes |
cafe backup <service> |
1 argument | yes |
cafe env <verb> |
1 argument | yes |
cafe gen <target> |
1 argument | yes |
cafe lock <path> |
1 argument | yes |
cafe mcp |
0 arguments | yes |
cafe reclaim |
0 arguments | yes |
cafe init |
0 arguments | flags only |
cafe new <name> |
1 argument | flags only |
Flags only means exactly what it says: the flags parse, the argument count is
checked, and the command returns not implemented in v0 with exit code 1.
doctor and dev both take an optional path — cafe doctor alone reports
the machine, and cafe doctor ./my-service reports the machine and whether it
can run this particular project.
Two things that will bite you
Section titled “Two things that will bite you”cafe init takes no positional argument. It operates on a directory. This
is the argument-count check firing:
$ cafe init my-saascafe: usage: cafe init wants 0 arguments, got 1 (usage: cafe init [flags])$ echo $?2The command that takes a name is cafe new:
$ cafe new my-saascafe: cafe new: not implemented in v0$ echo $?1Flags come before positional arguments. cafe uses Go’s standard flag
package, so the flags must precede them:
cafe deploy --dry-run identity # workscafe deploy identity --dry-run # usage error, exit 2Exit codes
Section titled “Exit codes”These are a contract — scripts branch on them, so they do not change without a version bump.
| Code | Meaning |
|---|---|
| 0 | the command succeeded, or you asked for help |
| 1 | the command ran and failed |
| 2 | cafe was invoked wrongly: unknown command, unknown flag, wrong argument count |
Step 4 — cafe init a project
Section titled “Step 4 — cafe init a project”Coming soon — the flags parse, the command
returns not implemented in v0.
$ cafe initcafe: cafe init: not implemented in v0$ echo $?1cafe init is the command that will create a cafaye.yml for the current
project. It takes -dir (default .) and -force, and nothing else:
cafe help initcafe init - create a cafaye.yml manifest for the current project
Usage: cafe init [flags]
Flags: -dir string directory to initialize (default ".") -force overwrite an existing cafaye.ymlUntil it ships, write the manifest by hand. Copy the shape from
Contracts, then prove it with the one validator that does work.
docs/cafaye.yml is a complete, schema-valid example you can read in the
repository.
cafe contract lint ./cafaye.ymlOK ./cafaye.ymlPoint it at a directory and it walks the tree, checking every cafaye.yml it
finds and skipping .git, node_modules, deps, _build, and target:
cafe contract lint /path/to/checkoutsOK /path/to/checkouts/billing/cafaye.ymlINVALID /path/to/checkouts/parlor/cafaye.yml: is missing required fields ["name", "language", "core", "repository", "owner"]It prints one line per manifest and nothing else, so the output is greppable and diffable. It exits 1 if any manifest is invalid, and also if the path holds no manifest at all — a tree that validated nothing is not a pass:
$ cafe contract lint /tmp/empty-dircafe: cafe contract lint: no cafaye.yml found under /tmp/empty-dirValidation is two things: the core JSON Schema, vendored into the cafe binary
at internal/contract/schemas/manifest-0.2.json and pinned by sha256 (cafe
never fetches it), plus the three cross-field rules a JSON Schema cannot state —
a published event type must start with the publisher’s own name, a service never
consumes its own events, and an exposes must name an OpenAPI document or an
event. The schema is checked first; the cross-field rules only run on a document
that passed it.
Checking a core version constraint
Section titled “Checking a core version constraint”cafe contract resolve answers one question — does this service’s core: range
admit this core release — and is shaped for a CI gate.
cafe contract resolve '^0.2.0' 0.2.0yes ^0.2.0 allows 0.2.0: 0.2.0 is in [0.2.0, 0.3.0)cafe contract resolve '^0.1.0' 0.2.0no ^0.1.0 allows 0.2.0: 0.2.0 is not in [0.1.0, 0.2.0)Exit 0 when the version is inside the constraint, 1 when it is not, 2 when
cafe cannot read what you typed. The constraint grammar is deliberately tiny:
1.2.3 exactly, ^1.2.3 caret, ~1.2.3 tilde, >=1.2.3 at-or-above. No
ranges, no ||, no x-ranges. Note the pre-1.0 caret: on a 0.x service
^0.2.0 is >=0.2.0 <0.3.0, so a 0.3.0 core is not admitted by
^0.2.0.
Step 5 — run a service locally
Section titled “Step 5 — run a service locally”cafe dev works. It reads a project’s cafaye.yml, validates it against the
same rules cafe contract lint applies, renders a compose file from it, writes
that file and prints it in full, brings the stack up, waits for it, and reports
what came up and what did not.
cafe dev --dry-run ./hello # render and print; do not bring anything upproject hello-dev: hello (go)skipped legacy: optional dependency, and the local registry does not know how to run itwrote /work/hello/hello.compose.yamlThe report is the message: cafe dev prints its verdict for a stack that came
up with something in it that did not, on stdout, once, and does not repeat it on
stderr. A generated compose file is marked do not edit — every run rewrites it,
and the manifest is the source of truth.
What it does not do yet: it plans from the manifest and the local registry, so
a dependency the registry does not know how to run is reported as skipped
rather than silently omitted. It does not deploy, and it does not start a broker —
nothing in the platform publishes events off the outbox.
With the service actually up — through bin/dev or the two-file merge — both
probes answer:
{"status":"ok"}{"status":"ok","deps":"postgres"}Without a database readyz answers {"status":"ok","deps":"none"} — there is
nothing to check, and saying so beats an empty list that looks identical to a
healthy dependency check. The /v1 routes are only registered when
DATABASE_URL is set, so a missing database is a clean 404 rather than a
pile of 500s.
To run identity without Docker at all — the faster loop while you are changing
code, and it needs no database:
mise install # reads identity/mise.toml -> the pinned Gogo run ./cmd/identity # http://localhost:8080The other services
Section titled “The other services”The command depends on which kind of stack the repository ships, and the two kinds are not interchangeable — that is the whole table:
| Service | Local stack | Command | Port | What answers |
|---|---|---|---|---|
identity |
kit override |
bin/dev — see the caution above |
8080 | {"status":"ok","deps":"postgres"} |
courier |
kit override |
bin/dev, or mix phx.server — see the caution above |
4000 | {"status":"ok"} |
billing |
kit override |
bin/dev — see the caution above |
3000 | {"status":"ok","checks":{"database":"ok"}} |
darkroom |
kit override |
two-file merge, psql migrations, go run ./cmd/darkroom |
8080 | {"status":"ok"} |
muse |
kit override |
two-file merge (needs MUSE_VAULT_KEY), psql migrations |
8000 | {"status":"ok","deps":{"db":"ok"}} |
gateway |
kit override |
two-file merge | 8080 | {"deps":{…}} |
parlor |
none — no Compose file | npm run dev |
3000 | {"status":"ok","deps":"none"} |
The first three ship bin/dev and a kit.ref pin; the next three ship a
kit.ref but no bin/dev, so the two-file merge is yours to run. parlor
ships no Compose file at all — it is a Vite app and npm run dev is the whole
story.
Two stacks refuse to start until you tell them a secret, and that refusal is
the right one: muse will not render without MUSE_VAULT_KEY, and courier
will not render without COURIER_SECRET_BOX_KEY. (courier also reads
COURIER_INBOUND_RESEND_SECRET, but that one has a default — it is not a
refusal, just a placeholder you should replace.) The compose message names the
variable and, for muse, the command that generates a valid one.
billing is the one that used to be described as “database only, application on
the host”. It is not: its Compose file builds and publishes billing on
3000:3000 like every other service, and bin/dev brings the whole thing
up. The commands are in that repository’s README if you want the host-side
loop instead.
Which bring-up command is which
Section titled “Which bring-up command is which”The table above is per-service local development.
When somebody says “bring up the platform”, there are three different stacks,
and they are not interchangeable — with four commands, because install.sh
drives bin/launch rather than being a stack of its own:
bin/launch (in site) is the launch tier — the product’s local face.
It brings up the launch four (identity + gateway + courier + parlor) on
kit’s pinned stack (one Postgres cluster, NATS with JetStream, Redis, the OTel
collector), plus an SMTP sink a person can read (mailpit), built from the
checkouts beside site.
scripts/walk-the-loop.sh then walks the loop the product is sold on against
that stack — signup, the confirmation mail, confirm, signin, signin with MFA,
forgot password, the reset mail redeemed, create-an-account/invite-a-member,
settings — with every mail read out of mailpit over its HTTP API.
COURIER_FORCE_SSL is unset there on purpose: the launch network is plain HTTP
(see courier).
bin/launch # fetch kit, mint the courier key, up --wait, print URLsscripts/walk-the-loop.sh # the loop, with real curls against the launch stack| What | Host port | Container port |
|---|---|---|
identity, the auth service |
${LAUNCH_IDENTITY_PORT:-15080} |
8080 |
gateway, the API edge |
${LAUNCH_GATEWAY_PORT:-15081} |
8080 |
courier, the mail service |
${LAUNCH_COURIER_PORT:-15082} |
4000 |
parlor, the product UI — the front door |
${LAUNCH_PARLOR_PORT:-15083} |
3000 |
mailpit web UI, the SMTP sink (COURIER_SMTP_HOST: mailpit, port 1025 inside the network) |
${KIT_MAILPIT_PORT:-15025} |
8025 |
The port block is kit’s 15000-15999, declared in
site/e2e/docker-compose.launch.yml, which is an override on kit’s stack —
read that file’s header before changing anything about the topology.
install.sh (in site) is the stranger path onto the launch tier.
It clones the launch four (identity, gateway, courier, parlor — at their default branch tips, because the tier builds whatever is checked out) plus kit at master beside the site checkout, then drives bin/launch — so what comes up is the launch stack above, not a second topology: the launch four on kit’s pinned stack (one Postgres cluster, NATS with JetStream, Redis, the OTel collector), plus the mailpit SMTP sink.
It is safe to re-run: a checkout that is already beside site is left alone, and a stack that is already up is reported with its URLs rather than rebuilt.
When it finishes, bin/launch prints where to look — parlor on 15083 (the front door), mailpit on 15025, identity on 15080, gateway on 15081, courier on 15082 — including the launch owner’s sign-in for the stack it just brought up.
It does not clone pantry or oauth-stub: those are the e2e tier’s services, and the launch compose names neither.
bin/up (in site) is the e2e tier, not the launch stack.
It brings up identity + gateway + pantry + oauth-stub (+ site) from
published artifacts, on its own 16000-16099 port block — so the two tiers can
run on one machine without meeting.
Do not describe it as bringing up the launch tier: it runs pantry and
oauth-stub, which the launch stack does not contain, and it does not run
courier, parlor, NATS, Redis, or mailpit in the launch shape.
curl https://cafaye.com/box/install | sh (kit) is the production path.
It installs the substrate — k3s + Traefik + cert-manager + ArgoCD +
CloudNativePG + Redis + Alloy/Loki/Grafana + Prometheus — on one box.
Deployment is the runbook for it.
The ports, and the one collision that is not a port collision
Section titled “The ports, and the one collision that is not a port collision”| 8080 | identity, gateway, darkroom — three services, one port |
| 3000 | billing, parlor — two services, one port |
| 15500 | the Postgres every kit override uses |
No service publishes its own Postgres host port. darkroom and muse used
to bind 5432 and 5433; they no longer do — their postgres: services carry no
ports: at all, and the cluster they share with identity, courier and
billing is the one kit publishes, on ${KIT_POSTGRES_PORT:-15500}. The
variable that moves it is KIT_POSTGRES_PORT, it is kit’s rather than a
service’s, it lives in kit’s .env, and it moves kit’s shared container
rather than any service’s. Set it there if 15500 is taken.
The real reason you run one of identity, courier and billing at a time
is not a port. They share one Postgres container and each names its own
database — KIT_POSTGRES_DATABASES: identity, then courier, then billing —
and compose merges those overrides last-one-wins. Bring two up together and one
service’s DSN points at a database that does not exist, which surfaces as
role "…" does not exist and looks exactly like a wrong password. darkroom
and muse share that same cluster, so the same rule holds for them.
To stop one, without losing its data: bin/dev down. bin/dev nuke is the
destructive one and says so.
darkroom’s compose stack defaults to DARKROOM_OBJECT_STORE=memory, which is
what makes the local path work with no bucket and no credentials. Its
deployment build defaults to the s3 object store — docker/Dockerfile sets
DARKROOM_OBJECT_STORE=s3 — so docker build needs no flag to get it (see the
container table below).
Migrations are a step you run, not something that runs itself
Section titled “Migrations are a step you run, not something that runs itself”Every service applies its schema separately, in its own language, with its own tool. None of them migrates on boot — a rolling deploy with two versions live would race, and a half-applied migration would take the process down with it.
On bin/dev you do not run this step at all: bin/dev runs the
repository’s own migration command between bringing the stack up and printing the
URLs, and refuses to do anything else if the stack is not healthy first. The
commands below are for cafe dev, for a host-side loop, or for a Kamal deploy —
where they are a job that runs before the new image rolls out.
identity uses goose, which is a command-line tool rather than a module
dependency. The port is 15500, not 5432 — that is kit’s Postgres, which is
where bin/dev put it:
export DATABASE_URL=postgres://identity:identity@localhost:15500/identitygoose -dir migrations postgres "$DATABASE_URL" status # what has been appliedgoose -dir migrations postgres "$DATABASE_URL" up # apply everything pendingbilling uses Rails migrations, against the same Postgres and the same port:
DATABASE_URL=postgres://billing:billing@localhost:15500/billing bin/rails db:preparemuse applies plain SQL:
psql muse -f migrations/00001_outbox_events.sqlpsql muse -f migrations/00002_vault_secrets.sqlIn production, run up-by-one (or the equivalent) as a single ordered job
before the new image rolls out, and fail the deploy on a non-zero exit.
On a cafe dev stack, run them from inside the compose network. The
rendered file publishes only the service’s own port, so goose on the host has
nothing to connect to, and the first request you make before migrating fails with
a bare 500 whose real reason — relation "users" does not exist — is only in
the service’s logs. Hosted pilot onboarding
has the loop that was run, and why it does not record a goose version.
Step 6 — deploy to your own infrastructure
Section titled “Step 6 — deploy to your own infrastructure”cafe deploy works. It drives Kamal: it installs Docker on the host if the host
lacks it, boots the accessories, builds and pushes the image, and rolls the
release out behind kamal-proxy only once the new container answers its
healthcheck. What it does not have is a cafaye-operated remote, so it deploys
to infrastructure you already pay for.
cafe deploy --dry-run identityRun it from the project directory — the one holding config/deploy.yml. In a
directory without one, the real answer is:
cafe: cafe deploy: /tmp/cafdry has no config/deploy.yml, so there is nothing to deploy. Copy it from cafaye/kit at the ref your kit.ref names: kit/templates/kamal/deploy.yml.erb -> config/deploy.ymlIt takes -env, -version, -dry-run and -yes, and its usage line is
cafe deploy [flags] <service>. Flags come before the service name, so
cafe deploy --dry-run identity and not cafe deploy identity --dry-run; the
latter is a usage error. -env names an overlay merged over the base config
(config/deploy.<env>.yml over config/deploy.yml) and defaults to empty
rather than to a named stage. -dry-run prints every command it would run and
changes nothing. Without -yes it asks first, and a machine with nobody to
answer is told to pass it rather than left to guess.
And, independently of all that, you can still build and run an image yourself. Every service ships a multi-stage, non-root Dockerfile, and they are not interchangeable — each pins its own base image and runtime:
git clone git@github.com:cafaye/identity.gitcd identitydocker build -t identity .docker run --rm -p 8080:8080 \ -e DATABASE_URL='postgres://identity:***@postgres:5432/identity?sslmode=require' \ -e LOG_LEVEL=info \ identityThe images run as uid 65532 on gcr.io/distroless/static-debian12:nonroot, so
there is no shell in the container. That is deliberate, and it means you cannot
docker exec sh into a running service to debug it: use the probes and the
logs.
| Service | docker build |
Runtime port | Base |
|---|---|---|---|
identity |
docker build -t identity . |
8080 | gcr.io/distroless/static-debian12:nonroot |
billing |
docker build -t billing . |
80 | ruby:4.0.1-slim, entrypoint /rails/bin/docker-entrypoint |
courier |
docker build -t courier . |
4000 | debian:trixie-20260918-slim, USER nobody |
darkroom |
docker build -f docker/Dockerfile -t darkroom . |
8080 | a Go binary on the same distroless base as identity; the Dockerfile is under docker/, not the root |
muse |
docker build -t muse . |
8000 | python:3.14-slim |
gateway |
docker build -t gateway . |
8080 | gcr.io/distroless/static-debian12:nonroot |
parlor |
docker build -t parlor . |
3000 | node:22.22.2-slim (pinned in every stage), ENV PORT=3000 |
Two of those build lines used to carry flags that do not exist. courier’s
Dockerfile declares no SERVICE_NAME build argument, and its compose file passes
no args:, so --build-arg SERVICE_NAME=courier builds the same image as
without it while looking like it configures something. And --build-arg --features s3 is not a command at all: --build-arg requires a NAME[=VALUE]
after it, so Docker refuses at the flag parser before reading any Dockerfile.
$ docker build --build-argflag needs an argument: --build-argdarkroom needs no flag for its deployment build either way: docker/Dockerfile
already sets DARKROOM_OBJECT_STORE=s3, so the default build is the S3
build. There is no dev-auth verifier to enable — the Go tree’s only verifier
is auth.New, and it is always on.
The order that works, and the order that bites:
- Migrate first, as a job. The service never migrates on boot. A schema the new image needs must exist before the new image starts.
- Roll out. The probes below decide whether traffic is held back.
- Read
/readyz, not/healthz. Liveness answers whenever the process can dispatch. Readiness answers 503 while the service cannot do work. Restarting on a readiness failure is how a dependency outage becomes a crash loop.
| Service | Liveness | Readiness |
|---|---|---|
identity |
GET /healthz → always 200 |
GET /readyz → 200, or 503 with each probe bounded at 2s |
billing |
GET /healthz → always 200 |
GET /readyz → 200, or 503 {"status":"error","checks":{"database":"error"}} |
courier |
GET /healthz → always 200, never touches the database |
GET /readyz → 503 while the database is unreachable |
darkroom |
GET /healthz → consults nothing |
GET /readyz → really runs a query |
muse |
GET /healthz → 200, consults nothing |
GET /readyz → reports the db slot |
gateway |
GET /healthz → always 200 |
GET /readyz → 200, or 503 if a registered probe is down |
parlor |
GET /healthz → 200 |
GET /readyz → {"status":"ok","deps":"none"} |
One warning about courier: with a database that has gone away underneath a
running pool, /readyz takes about 4.4 seconds to answer 503 — the pool’s
queue backpressure, not the query timeout. An orchestrator with a shorter probe
timeout will time out and read courier as not ready, which is the same verdict.
Set the probe timeout above 5s or you will restart a service that is answering
correctly.
gateway’s probes sit on the same port as its API, and it is not in the
table above for /v1/* because gateway forwards only what ROUTE_TABLE
configures: unset, it serves its own routes and nothing else. /v1/me is one of
its own, and it proves the auth chain without forwarding anything.
What is not in this path yet
Section titled “What is not in this path yet”The broker. core specifies a transactional outbox and NATS as the transport,
and identity, billing, muse, courier and darkroom all write
outbox_events correctly, in the same transaction as the domain change — but
no service starts a publisher loop, and identity’s only Publisher
implementation is a deliberate no-op that would mark every event published and
drain the outbox into nowhere. Events land in the table and stay there.
That is not a bug to work around; it is the state of the platform, and it is the first thing to know if you are waiting for a downstream reaction that never comes. Troubleshooting has the entry, and Upgrading opens by saying how much of the event surface this actually affects — it matters if you wrote your own publisher loop, and not at all if you did not.
Observability is instrumented and not deployed, and those are two different
sentences. core ships seven telemetry schemas and enforces the contract;
courier, billing, identity and muse each wire an OTel SDK and export
traces to whatever <SERVICE>_OTEL_ENDPOINT names, defaulting to the collector
that ships with the stack. No collector is deployed anywhere in a cafaye
environment, so a service started from its own docker-compose.yml is
exporting to a host that is not there.
Getting a collector takes bin/dev, which three of the seven repositories
already carry (identity, courier, billing) — it fetches kit’s whole stack
at the ref in kit.ref, brings it up, and prints the URLs. The other four have
no bin/dev, and copying it is the fix: cp <kit>/templates/bin/dev.sh ./bin/dev
(plus a kit.ref pinning a 40-character commit sha, because bin/dev refuses a
branch name before it makes any network call).
Observability has the per-service table and the three states,
and says what the gap costs you.
What to read next
Section titled “What to read next”- Hosted pilot onboarding — the same path, aimed at somebody who has already decided, with the licence position and every gap stated before you commit.
- Upgrading — if you already run a deployment and are moving to the current contracts.
- Architecture — what each service owns, and why the boundaries are where they are.
- Topology — the operator’s table: ports, probes, environment variables, the HTTP surface of each service, and the cross-repo drift audit.
- Contracts —
cafaye.yml, the event envelope, OpenAPI. The one thing worth understanding before anything else. - Observability — the telemetry contract, and the gap between it and a running stack.
- Services — one page per service, each stating what is built and what is not.
- Runbooks — provisioning, backup and restore, secret rotation, a service down, and billing webhooks. Read them before the first incident, not during it.
- Running the gates — the real gate for every repository, including the tiers that do not run by default.
- Troubleshooting — keyed by symptom.