Skip to main content
You can run Receipt as a multi-service stack behind a load balancer, or as the whole stack on one machine. Both are driven by the same service graph and both promote images tagged with the commit they were built from — so what you learn on one shape transfers to the other. This page describes the shape of a Receipt deployment: what runs, what the deploy refuses to do, and the release discipline the repository enforces on itself. It is not a copy of any one environment’s configuration, and it deliberately carries no account, host or resource identifiers.

Two topologies

Multi-service. The web/gateway, each runtime role, the sync layer, the durable-execution server, the integration provider and the channel adapters run as separate services behind a load balancer, with managed Postgres and managed Redis, and shared network storage for the runtime data directory and the sync replica. Services scale independently. Single-host. The whole stack runs on one machine under a container compose file, with a reverse proxy terminating TLS in front of it and the gateway published only on 127.0.0.1:3000. Redis runs as a container instead of a managed service, host directories replace network storage, and a single sync process replaces the replication-manager and view-syncer split. Both shapes are wired by the same service graph, which is why routing behaves identically. Beyond routing they are genuinely different topologies. The multi-service shape buys independent scaling and managed data services; the single host buys one instance whose entire definition is a compose file and an environment file, at the cost of scaling any part of it separately. Connector coverage is the same on both — it comes from the checked-in provider catalog, not from the topology. The repository’s own guidance treats an in-place rollout on the single host as the default for routine application fixes, and says not to reach for the full infrastructure path unless an operator explicitly asks for it.

Addressing

How one service reaches another is deployment-defined — service discovery in the multi-service shape, compose service names on a single host. What is not deployment-defined is the routing table: a single declarative service graph fixes the route prefix, internal port, exposure and health path for every service, which is why local and deployed behave the same. See Processes, roles, and routing for that table. The endpoints you call yourself are few: The web application answers GET /health with {"ok":true}, and HEAD /health with an empty 200. Private services in the graph — the runtime’s own prefix, the durable-execution server and the sandbox controller — answer a bare 404 Not found to public requests, so a 404 there is the gateway working, not a broken service.

What the preflight enforces

The deploy preflight is a hard gate, run as its own step before any image is built: If the container daemon is not running, the failure line starts with Docker daemon check failed., repeats the daemon’s own first error line, and ends with this hint:
A passing run prints [preflight] Environment is ready. A failing run prints [preflight] Environment is not ready: followed by one bullet per unmet requirement. Promoting an existing release skips this step along with the image build, because there is nothing left to build. The release rules below still apply.

Public origin rules

Set the public origin before you deploy, not after. Better Auth, the sync layer, Receipt Connect, the integration provider and the channel adapters all derive URLs from it, and they must agree.
  • The deploy refuses to start without one. You give it either a public domain or the full public base URL; a missing value fails with RECEIPT_PUBLIC_DOMAIN or RECEIPT_PUBLIC_BASE_URL is required for AWS deploys.
  • The canonical base URL is validated. It must be HTTPS. localhost is rejected. So is a CDN’s default hostname — put your own domain in front instead.
  • If your DNS is not managed in the deploy account, issue and validate the certificate yourself and point the deploy at it with RECEIPT_PUBLIC_DOMAIN_CERT_ARN.
  • Trusted origins are validated strictly. The canonical origin must be an exact HTTP or HTTPS origin with no credentials and no path, query or fragment. Every legacy origin you list in RECEIPT_LEGACY_PUBLIC_ORIGINS must additionally be HTTPS with no wildcard host.
BETTER_AUTH_URL must equal the public origin. It is the only origin the auth server trusts by default, and the session cookie’s Secure flag follows its scheme.A mismatch does not fail loudly at boot. It fails later, at sign-in, when the browser’s own origin is not on the trusted list or the session cookie is discarded — which is a much more expensive way to find out.Set VITE_BETTER_AUTH_URL to the same origin. It is inlined into the browser bundle at build time, and while the auth client deliberately calls /api/auth on whatever origin served the page, settings callback URLs are built from this value and it is rejected outright if it is not an absolute HTTP or HTTPS URL, or if it points at a CDN’s default hostname.

The secrets a deploy needs

The deploy pipeline reads a secrets bundle from a secret store you control and pushes images to a container registry you control. Described by role, the bundle must carry: Optional connector credentials — the per-connector OAuth application pairs and the channel-adapter credentials — live in the same bundle. Missing optional values do not block a deploy; they print Optional secret values are empty: <names>. Related integrations may not work.
Every base64 key in the bundle is length-validated before upload: it must decode to exactly 32 bytes, or the loader refuses it with <name> must be a base64-encoded 32 byte key. The provider’s environment secret key is validated to be a UUID v4 in the same pass.Both AES keys are irreversible once data exists. Configuration, secrets, and keys explains why rotating one destroys what it protects.

Release discipline worth copying

Four rules the repository enforces on itself, worth adopting whatever you deploy onto.
A release manifest is validated before it is applied, and any mutable reference is rejected:
A :latest tag fails. So does a build-asset reference. If you cannot name the exact image, you cannot deploy it.
The manifest is a versioned document carrying the release tag, the full 40-character commit SHA, the git ref, the stage it was built for, where the source archive came from, when it was created, and one immutable reference per service image.Applying a manifest turns off image building entirely — the deploy promotes exactly those references rather than producing new ones. A production deploy refuses to run without a release tag or a manifest location: it must promote an immutable release, never rebuild one.
The launcher refuses a detached HEAD, fetches the remote ref, and requires the commit being deployed to be an ancestor of it. The comment in the code records why: production once ran an unpushed local commit while the remote branch was behind, and that commit was later lost.There is an override flag. Treat needing it as a signal, not a step.
On the multi-service shape, the postdeploy phase launches a one-off task from the gateway task definition whose command is the migration runner, waits for it to stop, and fails the deploy on a non-zero exit — before it waits for services to become stable. The infrastructure deploy has already rolled the services by the time that task runs, so the migration gates whether the deploy succeeds, not the rollout itself.On the single-host shape, migrations run at host bootstrap, and again — ahead of the containers being recreated — on every rollout that changes the gateway image.See Postgres, migrations, and the sync publication.
The deploy also validates itself afterwards. It asserts each service’s required environment variables and process role, that public URLs are HTTPS and not localhost, that no deprecated integration variable aliases are present, that required services have at least one running task, and that the sync cache URL matches the public origin and answers.

Email

Receipt does not operate an email service. Outbound mail goes through an account you own, and that account needs a verified sender identity. For production stages the deploy has an explicit guard. It requires AUTH_EMAIL_PROVIDER=ses and VITE_DISABLE_EMAIL_VERIFICATION_OTP=false, and it checks both production sending access and a verified sender identity before it will deploy. An account still inside the provider’s sandbox fails that check — move it out first. On a self-hosted build with email disabled, organization invitations fall back to copyable signup links in the invite dialog rather than failing.

Before you call it production

The reference deploy pipeline’s role is broadly scoped on purpose. It carries a single bootstrap statement granting all actions on all resources.That is a bootstrap convenience, not a steady-state posture. Scope the role down to what your deploys actually touch before you treat the deployment as production.
The infrastructure is more careful elsewhere, which shows what the target looks like: runtime services get no cloud permissions at all, except the two worker roles that start, stop and describe the sandbox host; the sandbox instance role gets registry read-only, managed-instance access and read on exactly one parameter; the single-host instance role gets those same two managed policies plus an inline policy covering one parameter, two buckets, instance start/stop and describe, and sending mail through identities in its own account.

Rollback

Rollback is re-promotion, not rebuilding. Next step: check that a deployment is actually up — the endpoints, logs and traces that tell you whether the release you just promoted is serving.