Skip to content

Hosted Pilot Onboarding

You have evaluated the platform, you have said yes, and you are sitting in front of a laptop with a credit card. This page is the whole path from nothing to something running that you can show your team, in order, with the failure branch beside every step. It is not a tour of the features.

Every command on this page was run against cafe at commit 460acf3 and identity at master, 35c2576, on macOS with Docker 29.4.0. Output is quoted where it matters. Where a step could not be run here, the page says so on the step rather than implying it was verified. The last section lists those. Both repositories move; the numbers on this page are a dated snapshot and the ones that matter are the ones you can re-run.

There are two ways to run cafaye, and one of them is ours. They are not the same product and they are priced differently.

Self-hosted is the first. You run the stack in your own infrastructure, on your own cloud account or on your own metal, and your Postgres, your bucket, your TLS terminator and your keys stay yours. It is free and complete, and nothing about it requires an account with us. What exists here is public repositories and a CLI.

Cafaye Ops is the second, and it is the one we operate. We provision and watch the box, patch it, prove your backups restore, hold its history, and apply upstream upgrades when they are safe to apply. It is a flat monthly price banded on countable things — applications deployed, accounts created, services enabled — with no usage metering and no per-box charge. You keep root, kubectl, ArgoCD, Grafana and the git repository, so it is your box with our attention on it rather than ours with your data in it. We do not page anybody yet: response is on a stated schedule during business hours, and the tier we sell says so.

Two things that have not changed. cafe deploy itself is implemented — it builds the image, pushes it and rolls it out — but it still has no cafaye-operated remote to target automatically, so it deploys to infrastructure you already pay for. And Cafaye Ops is a service rather than a remote: the box lives on your cloud account, and what you buy is the operation of it, not a destination for the command. The launch deploy path is the substrate (k3s + ArgoCD + CloudNativePG), decided in DEPLOYMENT.md in the moon repository; Kamal is the current implementation and retires at parity, not before. Deployment is its runbook.

So “pilot” means one of two things, and you should decide which before you spend an afternoon on it: we run it with you — we help you get it running and keep it running, and you own the machine — or we run it for you, which is Cafaye Ops and is billed monthly. If what you wanted was a vendor-operated platform with an SLA behind it, that is not what Cafaye Ops is yet, and we would rather say so now than in month two.

Read the what is not ready section before you commit. It is the honest list, and it is short.

Seven services, each in its own repository, each versioned on its own. Take the one you need rather than the whole platform.

Service What it owns Language The gap you will hit
identity Users, sessions, accounts, roles, invitations, OIDC provider, MFA Go Password reset and email verification are built and published, but answer 503 until courier’s mailer and a link template are configured. No refresh tokens, no admin API
billing Plans, customers, subscriptions, prepaid credit, Stripe in and out Ruby No customer portal, no refunds, no usage-events API
courier Transactional email, preferences, every outbound webhook Elixir No email provider adapter configured — nothing is delivered until you set one
darkroom Signed uploads, tenant-scoped assets, variants, S3 and R2 Rust No transcoding, no CDN, no malware scanning
muse LLM routing, encrypted credentials vault, token metering Python Auth was a stub until 2026-10-01. Since muse e3f53b0 it verifies the bearer against identity’s JWKS. Two claim names for one scope are still an open ruling
gateway The public edge: JWT verification, rate limits, static routing Go No API keys and no mounted scope check. The TypeScript implementation that had both was deleted at the Go cutover. It serves /v1/me, /auth/* and whatever ROUTE_TABLE names; unset, it forwards nothing
parlor Next.js app shell: signup, login, accounts, billing screens Next.js No admin surface, no e2e suite, and its cafaye.yml does not validate

Each has a page here that states what is built and what is not in its own words: Services. Read the status line before you rely on the row above — these rows are a summary, the service pages are the claim.

The split has landed, and a grant is written down in every repository. Measured 2026-10-10 from committed master, the fourteen public repositories carry a LICENSE. The framework — core, cafe, kit — is MIT, because its job is adoption and it should carry no strings. The seven services — identity, gateway, courier, darkroom, billing, parlor, muse — are under a Business Source Licence: free to run, free to self-host, never to offer a competing service. box and the artifact distribution it serves are proprietary: they are the product rather than a paywall placed in front of the code. pantry is closed by OPS-D10 (2026-10-10) — the public catalog is part of the operated product, so its source is proprietary while its service stays publicly reachable, with the contract still open. Licensing is the measurement, the command that re-derives every row, and the BSL’s parameters in full.

What the BSL does not grant is production use without a commercial licence. The Additional Use Grant is None, so the file permits development, evaluation and self-hosting, and then says you must buy a commercial licence or refrain from using the service (billing/LICENSE:13-16, billing/LICENSE:25-28). On the Change Date, 2030-10-10, each version becomes Apache-2.0. That is the question to settle before you build on it commercially, and it is a question for us rather than a blank on this page — the open questions are where unanswered money questions live.

muse changed grant twice, and anyone holding an earlier copy keeps it. It was AGPL-3.0-only, then MIT, and since 2026-10-01 it is BSL 1.1 with the rest of the services. The reversal from AGPL is deliberate and is stated in muse’s own README rather than left for a reader to discover by diffing the manifest (muse/README.md:594-608); a copy taken while pyproject.toml said AGPL-3.0-only is still AGPL-3.0-only, and BSL governs copies taken from here on.

What this section said on 2026-09-30, kept because the move is the point. The table below is a dated snapshot from before the grants landed, and it is superseded — every row of it is wrong today. It stays because it is the reason the Licensing page exists: ten of the thirteen repositories it covered stated no licence at all, which for a public repository means “all rights reserved” — not MIT, and not what we are offering.

Repository What it declared on 2026-09-30
cafaye-rb LICENSE.txt, MIT, “Copyright (c) 2026 cafaye” — the only repository with a licence file
darkroom, pantry license = "MIT" in Cargo.toml. No licence file.
gateway, docs "license": "MIT" in package.json. No licence file.
muse license = { text = "AGPL-3.0-only" } in pyproject.toml, and a README that says the same
identity, billing, courier, parlor, core, cafe, kit nothing. No licence file, no field in any manifest.

We have not published a sizing guide, and this page is not going to invent one. Here is what the code actually asserts, and it is all of it:

  • cafe doctor holds a floor of 4 GiB of memory and 4 CPUs for a local stack — a database, a cache and a service at once. That is a floor in cafe/internal/cli/doctor_env.go, not a recommendation, and it is the only number in the fleet about how much machine you need.
  • Every service is one stateless container. Multi-stage, non-root, no volume mounted. Replacing or restarting one loses nothing.
  • Five Postgres databases, one per service that owns state: identity, billing, courier, darkroom, muse. gateway and parlor own none. Postgres is the only thing a backup has to cover, except the bucket below.
  • Redis if you set REDIS_URL on gateway. Rate-limit counters live there; the browser session store does not, and that is a per-process Map.
  • One object-storage bucket you choose, for darkroom — S3 or Cloudflare R2, one implementation, two configurations. It is not covered by pg_dump.
  • One identity container brought up this way is three containers. Verified: cafe dev on identity renders identity plus postgres:16-alpine plus redis:7-alpine.

There is a managed option — Cafaye Ops — priced as a flat monthly band on countable things rather than per seat, per box or by usage. The ladder’s prices are still placeholders, and pricing is where that is stated rather than smoothed over. Self-hosting is free and complete. If you would rather not own the machine, see the section above; you still keep root, kubectl, ArgoCD, Grafana and the git repository, and what you pay for is the operation rather than a licence.

By default, nothing. No telemetry is exported unless you configure it, no service dials a cafaye-operated endpoint, and there is no licence check, no update check, and no analytics in any of them. (docs.cafaye.com itself has no analytics and no third-party scripts either — that is a decision recorded in this repository, not an oversight.)

Four things will leave, and only if you turn them on:

Egress Who initiates it Your bucket, your account
Presigned upload and variant writes darkroom, to a bucket you name yours
LLM provider requests muse, to providers whose keys you put in the vault your provider contract
Stripe API calls and signed webhooks billing, when you configure Stripe keys your Stripe account
Email delivery courier, once you configure a provider adapter — none is set by default your provider

This is a check you can run, not a sentence to interpret.

Terminal window
go install github.com/cafaye/cafe/cmd/cafe@latest
cafe doctor

There is no tagged release, so @latest resolves to a pseudo-version built from master. That works today, and it is the only install path: there is no Homebrew tap and no cafe install.

cafe doctor prints two tables and always exits 0 — it is a report about a machine, not a gate, so a missing row is something you read rather than a CI failure.

The first table is the toolchain. Ten rows, in this order, each resolved on PATH and never executed:

tool status found at
git ok /opt/homebrew/bin/git
docker ok /usr/local/bin/docker
docker compose ok /usr/local/bin/docker-compose
tilt ok /Users/kaka/.local/share/mise/shims/tilt
go ok /Users/kaka/.local/share/mise/shims/go
ruby ok /Users/kaka/.local/share/mise/shims/ruby
elixir ok /Users/kaka/.local/share/mise/shims/elixir
python ok /Users/kaka/.local/share/mise/shims/python3
bun ok /Users/kaka/.local/share/mise/shims/bun
rust ok /Users/kaka/.cargo/bin/rustc
checked 10 tools, 10 ok, 0 missing

That list is not an opinion on this page. It is the tools table in cafe/internal/cli/doctor.go, one entry per row, and the order above is the table’s order. python resolves python3 then python; rust resolves rustc then cargo. If a service you want adds a language, that table is where the requirement comes from.

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 bun.

The second table is the one the first cannot answer — can this machine run this project:

project /path/to/identity: identity (go), 3 services in identity-dev
check status detail
container runtime ok /usr/local/bin/docker
runtime running ok server 29.4.0
memory ok 16 GiB, need 4 GiB
cpu ok 8, need 4
port 8080 free -
toolchain go ok /Users/kaka/.local/share/mise/shims/go
checked 6 project checks, 6 ok

Read it before step 4, because three of its rows are invisible from the first table:

  • runtime running asks the Docker server, not the binary. A stopped Docker Desktop is still on PATH, so the tool table says ok while nothing can start. A failure here reads unreachable and says to start the runtime.
  • memory and cpu are the 4 GiB / 4 floor. A machine reports every toolchain it needs and still cannot run a stack.
  • port 8080 free. If something else holds it, cafe dev -port publishes the project service somewhere else — but the databases and caches do not move, so a collision on a database port is a different problem. On a cafe dev stack the database is not published on a host port at all (see step 5), which is why the collision you hit here is a service port.
Terminal window
go install github.com/cafaye/cafe/cmd/cafe@latest
cafe version
cafe 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.

If that version line says anything else, you have a tagged release, which is better — carry on.

If it says cafe: command not found, the install worked and your PATH does not. go install writes to $(go env GOBIN), which is $GOPATH/bin for a system Go and somewhere else entirely under a version manager — on the machine this page was written on, GOBIN is a mise directory and $GOPATH/bin is not on PATH at all:

Terminal window
go env GOBIN
export PATH="$(go env GOBIN):$PATH"

Do not assume $HOME/go/bin. On that machine it exists, it is not on PATH, and it holds a cafaye-cli from months earlier that is not this program.

Terminal window
git clone git@github.com:cafaye/identity.git
cd identity

Why identity first. It declares no dependency on another cafaye service, which means the whole stack is one service plus its database plus a cache. darkroom is the other one in that position, and courier and billing declare empty lists; muse and gateway reach identity. Get this one working, then add the others.

If the clone fails on SSH, you do not have a key registered on GitHub. Use HTTPS for a read-only clone if you have to — git clone https://github.com/cafaye/identity.git — but keep SSH for anything you push. PLAN.md §1 makes SSH the rule for repositories cafaye owns.

If you are adding your own service rather than running ours, cafe new and cafe init are stubs — they parse their flags and return not implemented in v0 — so write the manifest by hand. The minimum that validates is this, and it was linted:

name: pilot-app
description: A pilot's own application.
language: typescript
core: ^0.2.0
dependencies:
- name: identity
version: ^0.1.0
required: false
repository:
url: git@github.com:acme/pilot-app.git
defaultBranch: master
visibility: private
owner:
team: platform
contact: platform@acme.example

language must be one of go, ruby, elixir, python, typescript, rust, spec — core’s schema has no value for a repository with no server in it. Put your real coordinates in repository and owner; the ones above are placeholders.

Terminal window
cafe contract lint ./cafaye.yml
OK ./cafaye.yml

One line per manifest and nothing else, so it is greppable and diffable. Exit 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.

If it says INVALID … is missing required fields ["name", "language", "core", "repository", "owner"], you are looking at the shape parlor still has. Copy the keys from step 2 rather than trying to work out which one is missing.

Point it at a directory and it walks the tree, skipping .git, node_modules, deps, _build and target. On a whole checkout of the platform it prints one line per manifest, not per repository — pantry alone contributes ten, because its registry ships a copy of every service’s cafaye.yml. On the workspace this page was written against that was 31 manifests, 26 OK, 5 INVALID, exit 1, and only one of the five is a defect: parlor’s. Three of the other four are core’s own negative test fixtures, which exist to be rejected. Topology has the table — and re-run it, because that number moves with every packet and the answer underneath it does not:

Terminal window
cafe contract lint /path/to/cafaye
Terminal window
cafe dev --dry-run . # render and print; start nothing
cafe dev .

--dry-run first is not optional politeness: it prints the rendered compose file in full, so you can see what cafe dev decided before it decides it for you. On identity it renders three services — identity built from the repository’s own Dockerfile, plus postgres:16-alpine and redis:7-alpine — and writes cafe.dev.compose.yaml into the project.

Then bring it up. This builds the image, so the first run takes minutes:

starting 3 services in identity-dev
service status origin notes
postgres healthy infrastructure Up 10 seconds (healthy)
redis healthy infrastructure Up 10 seconds (healthy)
identity running project Up Less than a second
identity is up on http://localhost:8080

cafe dev leaves the stack running when it exits. To stop it:

Terminal window
docker compose -p identity-dev down

which takes the containers and the network down and leaves the volumes, so your data survives. Add -v when you want the database gone as well.

Four ways this step fails, and what each one means:

  • nothing to run: … has no docker/Dockerfile and the local registry names no image for it. You are in a directory without a Dockerfile. cafe dev builds your service from the project directory; there is no registry entry for a repository it has never seen.
  • skipped legacy: optional dependency, and the local registry does not know how to run it. Your manifest declares a dependency on another cafaye service. cafe dev resolves dependencies through a catalog and the default catalog is empty — pantry serves the official one over HTTP, but cafe dev -registry reads a local JSON file, and no catalog file is published yet. Run each service in its own directory until one is.
  • A container never reaches healthy. Read docker compose -p identity-dev logs. cafe dev waits three minutes by default (-wait) before it reports what did not come up, which is generous because a first run is mostly a build.
  • port 8080 in use. cafe doctor predicts this one. cafe dev -port 8081.

cafe dev does not run migrations, and it does not publish the database on a host port. Both of those matter, and together they make this the step a stuck reader stops at.

The stack is up and GET /readyz answers {"status":"ok","deps":"postgres"}, which is real — the pool is open and the query works. The schema is simply empty. The first request fails like this:

HTTP 500
{"type":"https://docs.cafaye.com/errors/internal","title":"Internal server error",
"status":500,"detail":"the request could not be completed. Quote the trace id
when reporting this.","instance":"/v1/users","code":"internal",
"trace_id":"7d45460a-1d11-4628-a658-7da100eee6f3"}

A bare 500 with no reason is the tell. The reason is in the service’s logs, and it names the actual problem:

level=ERROR msg="request failed" error="inserting a user: ERROR: relation
\"users\" does not exist (SQLSTATE 42P01)" trace_id=7d45460a-… method=POST
path=/v1/users

No service in this platform migrates on boot, and that is deliberate — a rolling deploy with two versions live would race, and a half-applied migration would take the process down with it. It is a deploy job, which locally means you run it.

Because cafe dev keeps Postgres on the compose network, run the migrations from inside it:

Terminal window
for f in migrations/0*.sql; do
printf '%s ' "$f"
awk '/^-- \+goose Up/{p=1;next} /^-- \+goose Down/{p=0} p' "$f" \
| docker compose -p identity-dev exec -T postgres \
psql -q -U identity -d identity -v ON_ERROR_STOP=1 \
&& echo ok || break
done
migrations/00001_init.sql ok
migrations/00002_users.sql ok
migrations/00003_sessions.sql ok
migrations/00004_outbox_events.sql ok
migrations/00005_accounts.sql ok
migrations/00006_account_users.sql ok
migrations/00007_account_invitations.sql ok
migrations/00008_connected_accounts.sql ok
migrations/00009_oidc_clients.sql ok
migrations/00010_mfa.sql ok

awk extracts each file’s Up section so the Down sections do not run behind it. ON_ERROR_STOP=1 is what stops a failure from scrolling past as a success.

Terminal window
curl -s localhost:8080/healthz
curl -s localhost:8080/readyz
{"status":"ok"}
{"status":"ok","deps":"postgres"}

deps: "postgres" is the one that matters. deps: "none" means DATABASE_URL is unset — no pool, no readiness dependency, and no /v1 routes at all, which is a clean 404 rather than an error.

Now create somebody and sign them in:

Terminal window
curl -s -X POST localhost:8080/v1/users \
-H 'Content-Type: application/json' \
-d '{"email":"pilot@acme.example","password":"correct-horse-battery-staple"}'
{"id":"5e1efb8c-240c-4cb1-9b14-1677238a04c9","email":"pilot@acme.example"}
Terminal window
curl -s -X POST localhost:8080/v1/session \
-H 'Content-Type: application/json' \
-d '{"email":"pilot@acme.example","password":"correct-horse-battery-staple"}'
{"token":"REDACTED","expires_at":"2026-10-30T12:23:14.540471907Z"}

The token is a 43-character base64url string — 32 random bytes, unpadded — valid for thirty days, redacted here because it was a real one from a throwaway database that has since been deleted. POST /v1/session also sets a __Host-session cookie to the same value, HttpOnly; Secure; SameSite=Lax; for a script, the bearer header is what you want.

Use it:

Terminal window
export TOKEN=$(curl -s -X POST localhost:8080/v1/session \
-H 'Content-Type: application/json' \
-d '{"email":"pilot@acme.example","password":"correct-horse-battery-staple"}' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
curl -s localhost:8080/v1/me -H "Authorization: Bearer $TOKEN"
{"id":"5e1efb8c-240c-4cb1-9b14-1677238a04c9","email":"pilot@acme.example"}

That is the first ten minutes. A service built from its own Dockerfile, a database it migrated itself, and an authenticated round trip.

What you will get when you check the database:

type | unpublished | attempts
--------------------------+-------------+----------
identity.account.created | t | 0
identity.user.created | t | 0

Two rows for two actions. unpublished: t is expected and is not a fault. See the next section.

Step 7 — check the two things this stack is not giving you

Section titled “Step 7 — check the two things this stack is not giving you”

Both of these surprise people, and both are one line of configuration rather than a defect to file.

The OIDC provider is not mounted. GET /.well-known/jwks.json returns 404 on this stack, and so does /.well-known/openid-configuration. Not because OIDC is broken — it is built and tested — but because Config.OIDCEnabled() requires all three of OIDC_ISSUER, OIDC_SIGNING_KEY and OIDC_SIGNING_KEY_ID to be set, and cafe dev sets none of them. A process with no signing key serves its /v1 surface and its probes and nothing else, deliberately, because there is no key to sign with and no document to publish.

Consequence for a pilot: gateway verifies bearer tokens against identity’s JWKS. With no JWKS, gateway has nothing to verify against. Until you set those three variables, call identity’s /v1 directly, which is what steps 5 and 6 do.

MFA is not mounted either, and it says so twice on startup — first the reason, then the consequence:

level=WARN msg="MFA_ENCRYPTION_KEY is not set"
level=WARN msg="MFA_ENCRYPTION_KEY is not configured; the MFA management routes
are not mounted and this process cannot verify a second factor for anybody"

GET /v1/mfa answers 404, which is the same fact from the outside.

MFA is built — TOTP enrollment, the login challenge, recovery codes, the whole surface, with its own OpenAPI document. MFA_ENCRYPTION_KEY is base64url, exactly 32 bytes, seals the TOTP secret at rest, and is never generated at boot. Unset means the management routes are absent; a value of the wrong length is a startup failure.

To turn it on locally, use the repository’s own stack rather than cafe dev, because cafe dev has no flag to add environment variables and rewrites its rendered file on every run:

Terminal window
cd identity
export MFA_ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '=\n' | tr '+/' '-_')
bin/dev

bin/dev, not docker compose up -d — this repository cannot start a database on its own. identity/docker-compose.yml is an override with no image: on postgres, because kit’s fetched stack ships that container, so docker compose up -d fails with “service "postgres" has neither an image nor a build context specified”. identity carries bin/dev, which fetches kit’s stack at the ref in kit.ref and merges this file beside it. You will also need to add MFA_ENCRYPTION_KEY to identity/docker-compose.yml’s environment: block, because bin/dev composes from that file and does not inject host variables into it.

Step 8 — provision a tenant, then put it somewhere that is not your laptop

Section titled “Step 8 — provision a tenant, then put it somewhere that is not your laptop”

You have a user. You do not have a customer.

Registration creates a personal account for that user in the same transaction — one nobody chose, named after them. Your tenant is the second account, a deliberate one with a slug somebody can put in a hostname. Both existing is normal, and it surprises people.

The full procedure, every response quoted from a live session, is Tenant provisioning. It is seven curl calls and ends with a verification step, because applying changes is not evidence that they worked.

Then read Backup and restore before you have anything in the database you care about. It is the runbook for the moment you find out you needed it last week.

For a real deployment, Getting started has the per-service build and run steps and the container table, and Topology has every port, probe and environment variable in one place.

Database-per-service. Each service that owns state has its own Postgres database, in your infrastructure, and no service reads another’s. There is no shared database, no shared schema, and no cross-service query. identity’s customers and billing’s customers are different rows in different databases that happen to share a uuid.

That is the whole tenancy story for the platform’s data, and it is why a service can be replaced, upgraded or scaled without coordinating with the other six.

The outbox, and why nothing arrives anywhere. Every service that records something worth telling another service about writes a row to its own outbox_events table in the same transaction as the domain change. That is the rule, and it is the reason a service cannot commit a change and then fail to record that it happened.

No service starts a publisher loop. identity’s only Publisher implementation is a deliberate no-op; starting it would mark every event published and drain the outbox into nowhere. So published_at stays null and attempts stays 0, as in step 6, and a cross-service reaction does not happen. An event in this platform today is a row, not a notification.

What this means for a pilot, plainly: if you are waiting for billing to react to a new signup, it will not, and it is not a bug in your deployment. Read the outbox yourself, or build the consumer loop that drains it. Both are legitimate; what is not legitimate is waiting.

courier’s outbound webhooks are the exception, and they are real. They are HTTP requests courier makes itself, with the Standard Webhooks signature headers, a retry budget and a circuit breaker — not events on a bus.

“Self-hosted” means what it says. Your Postgres, your bucket, your keys, your TLS terminator. Your backups, on your schedule. There is no cafaye-operated component in the data plane and no telemetry leaves unless you configure it; the one cafaye-operated component in the architecture is box, and it holds no readable customer secret — Security and trust argues it from the code. If you want us to run it for you, that is Cafaye Ops, the monthly operated tier: billed monthly, ladder prices still placeholders, sold by Stripe Payment Link and a human, like everything else on this site. pricing is the page.

Every line here is a fact about the current code, with where you can check it. This is the section to read before you commit, and to re-read in month two — because it will get shorter and this page will be updated when it does.

Not ready What it means for you Where to check
No broker. Nothing publishes events. Events accumulate unpublished. No cross-service reaction happens. outbox_events in any service; the publisher loop in core/docs/event-outbox.md
gateway has no API keys and no mounted scope check. The TypeScript implementation that had both was deleted at the Go cutover (f87c891, 2026-10-05), so a bearer JWT is the only script credential and no route refuses a token for lacking a scope. What it does do: verify RS256 bearers against identity’s JWKS, serve a browser login on a Redis session store, and relay the prefixes ROUTE_TABLE names — statically, with no discovery. Set ROUTE_TABLE or nothing is forwarded. openapi/v1.yaml (zero api-keys operations); internal/auth/middleware.go, where RequireScope is implemented and unmounted; internal/httpapi/table.go
gateway needs REDIS_URL for the browser surface. The session store is Redis and the four /auth routes are built only when IDENTITY_URL and REDIS_URL are both set, so a gateway without it serves no browser surface at all rather than a degraded one. browserSurfaceFromEnv in gateway’s internal/httpapi/runtime.go
muse’s auth was a stub until 2026-10-01, and is not one now. This row said do not put muse behind anything you care about. muse e3f53b0 verifies the bearer against identity’s JWKS with RS256 pinned at the decoder, requires iss, aud, sub, exp, iat, jti and account_id, checks the operation’s capability separately from the signature, and refuses unauthenticated traffic when identity is down. What is still true: muse accepts either of two authorisation claim names and refuses a token where they disagree, and that ruling is core’s to make. Security and trust; src/muse/auth.py; tests/test_auth.py
cafe has no stub commands as of 2026-10-10. The three that used to parse their flags and return not implemented in v0 — gen, init, new — are in the working table now, and the stub table is empty. cafe deploy works (Kamal) and cafe mcp serves the tools, so you can scaffold a manifest rather than writing one by hand. cafe/internal/cli/stub_test.go, whose stubCommands table is the single source of truth
Telemetry is instrumented; no collector is deployed. courier, billing, identity and muse each export traces, and nothing in a cafaye environment receives them. kit ships the stack and bin/dev runs it, so a pilot gets traces rather than a promise. You will not have a fleet-wide trace view until you run the stack, and you should budget for wiring <SERVICE>_OTEL_ENDPOINT at whatever backend you already pay for. muse is not on core’s error.type vocabulary, so do not group a cross-service error panel on that attribute. Observability
Password reset is built but unconfigured by default. POST /v1/password-resets and /confirm are served and published, and site ships the flow — but the recovery surface is mounted only with a mailer behind it, so without COURIER_BASE_URL, COURIER_TOKEN and PASSWORD_RESET_LINK_TEMPLATE it answers 503, not 404. Set those, and still plan a support channel. cmd/identity/main.go, buildRecovery; identity/openapi/v1.yaml
identity OIDC has no refresh tokens. Access tokens live fifteen minutes and cannot be renewed. the discovery document, where the absent features are absent rather than stubbed
No vault key rotation. MUSE_VAULT_KEY cannot be rotated in place. key_version is always 1
parlor is not a finished template, and its manifest does not validate. Do not clone it expecting a product shell. the drift audit
A green badge may have skipped its hard half. kit’s reusable workflow is callable and eight repositories now call it, but in most of the fleet the interesting tests are a second tier that does not run by default — so “the gate is green” often means somebody ran the suite by hand, that day. The exception is identity, whose CI refuses to skip its own database tier. Check what ran before you check the colour. Running the gates
The licence is a file in seven of the fourteen repositories, and a field in seven. Nothing that reads a manifest can answer what grant a repository carries in those seven, so the LICENSE file is the only declaration — and for billing, cafe, core, darkroom, gateway, identity and kit no package manifest exists in their ecosystem that could carry one, which is a fact about ecosystems rather than a gap. cafe contract lint will check it once core’s manifest schema has a field for it. the licence table and its re-derive it yourself command

One more that decides whether you can put anything in front of a customer: docs is the only cafaye repository with no CI workflow at all. The other twelve each have a .github/workflows/ci.yml on master. So a green badge somewhere in the fleet may still have skipped its hard half — and there is no badge to read at all on a docs commit, which is worth knowing when you ask us whether something is tested.

Two more that are not gaps but will still surprise you:

  • Nine of the ten event types identity writes have no payload schema in core, and three of the nine have no catalog row either — the two oidc_client types its manifest advertises, plus identity.member.accepted, which core spells identity.member.joined. So exactly one of the ten, identity.user.created, has both a published name and a published payload; six more have a name core published and a payload core never described. If you build a consumer on an identity event, you are most likely building on a payload that does not exist yet. Upgrading has the detail.
  • billing’s subscription and plan event payloads do not match core’s schemas. billing emits its own ids where core’s schemas describe the payment processor’s, and its own contract test says so out loud. Prefer GET /v1/subscriptions/{id} over the event payload as your source of truth.

Small, specific, and things only a design partner can give:

  1. A real workload on the platform — your users, your traffic, your data shapes. A synthetic benchmark tells us nothing about where the seams hurt.
  2. The first breakage. The first time something failed in a way you had to work around, tell us within a week. That report is the most valuable thing this arrangement produces, and it is worthless in month six.
  3. Two hours, twice, for the calls. One after you are running, one after you have hit something. We bring the agenda.
  4. A public write-up, if you are willing. Not required. Anonymised if you prefer, and we will never name you without asking.

What we do not ask: a case study you did not agree to, access to your customers’ data, or a reference call you have not prepared for.

  • A named human, and a response window. Not a queue. Agreed in writing before the pilot starts.
  • Direct changes, not a backlog ticket. If something in this platform is wrong, you say so and it gets fixed in that repository, with the commit.
  • A seat at the design decisions that affect you — the tenancy model, the event grammar, what the platform refuses to build — while they are still open.
  • Your feature requests weighed against a real install, which is worth more than a feature request from somebody who has never run the thing.

What exists today, honestly:

  • The repositories. https://github.com/cafaye — every service, its issues, and its history. This is the channel we actually watch.
  • A per-service contact address, declared in each repository’s cafaye.yml under owner.contact: identity@cafaye.com, billing@cafaye.com, courier@cafaye.com, darkroom@cafaye.com, gateway@cafaye.com, muse@cafaye.com, pantry@cafaye.com, cafe@cafaye.com, core@cafaye.com, cafaye@cafaye.com, docs@cafaye.com, parlor@cafaye.com, site@cafaye.com. One repository declares none: kit, which has no cafaye.yml at all, so there is no address to write to for it.
  • This site. Every command on it was run, and every gap on it was measured.

What does not exist yet, and we would rather say so than have you discover it: a support address, a status page, an uptime record, and an SLA. None of those is published anywhere on the org today. They come with the pilot agreement, in writing, before you start.

Stated here rather than left for you to guess:

  • Nothing on macOS with Docker Desktop was run on Linux. No step was run on a Kubernetes cluster, on ARM outside this machine, or against a managed Postgres. Steps 1–6 are one machine, one platform, one day.
  • cafe dev bringing up a project that depends on another cafaye service was not run, because no published catalog file exists for -registry to read. Step 4 states the error message the code produces for it; the message was read from internal/dev/registry.go, not captured from a run.
  • The MFA enrollment walkthrough was not re-run. The route table, the OpenAPI document and the startup warning above are all real and were read; the openssl one-liner in step 7 is a shape, not a verified recipe, and its 32-byte requirement is the thing to get right.
  • A production deployment was not performed. No docker build for each of the seven services, no rollout, no restore from a real backup.
  • Only identity was actually run. The other six services’ rows are read out of their repositories, not exercised. cafe dev --dry-run was run against darkroom as a second data point and rendered three services correctly — its Dockerfile path comes from its manifest, not from the project root, so cafe dev finds it — but nothing was brought up.
  • Sizing was not measured. Nothing on this page says how much memory or CPU a production install wants, because nobody in the fleet has published that and this page did not go and find out.
  • muse was not started. Its AGPL declaration was read out of pyproject.toml and its README; its behaviour was not exercised.
  • Billing was not run against Stripe. The lifecycle claims come from that repository’s README and its tests, not from a live processor.
  • The tenant-provisioning runbook was not re-run for this page. It was run live when it was written and every response in it is quoted from that session; re-running it was out of scope here, and it is linked rather than repeated.

If a step here is wrong, that is a bug in this page and it is worth an issue on the docs repository. If a step is missing, that is worse, and the same applies.

  • Getting started — the longer version of the path, with every service’s build and container table.
  • Tenant provisioning — the next thing you need, quoted from a live session.
  • Topology — every port, probe, environment variable and the cross-repo drift audit.
  • Upgrading — if you are already running something.
  • Services — what each one is, in its own words.
  • Running the gates — how to tell a real green from a skipped one.