Getting a self-hosted build
You have to build the web image yourself. The web Dockerfile,deploy/Dockerfile.receipt-web, accepts and re-exports the build argument, so add it to whatever build invocation you already use:
SELF_HOSTED_SETUP_TOKEN to the running container, so the first admin account can be created.
For a local checkout, set VITE_APP_INSTANCE_MODE=self_hosted in apps/start/.env.local and rebuild the web app. Turbo’s build task lists .env.local among its inputs and tracks VITE_APP_INSTANCE_MODE in its cache key, so the rebuild is not skipped.
Turbo runs in strict env mode, and
VITE_APP_INSTANCE_MODE is one of the seven variables in its globalEnv list, alongside NODE_ENV, VITE_BETTER_AUTH_URL, VITE_SELF_HOST_SOURCE, VITE_ZERO_CACHE_URL, VITE_ENABLE_ORGANIZATION_PROVIDER_KEYS and VITE_DISABLE_REDIS.Four things that do not exist until the build is self-hosted
Until the bundle is built withVITE_APP_INSTANCE_MODE=self_hosted, these are unreachable, no matter what you put in your environment:
- The
/setupclaim wizard. On a cloud build/setupredirects to/chat. - The setup token.
SELF_HOSTED_SETUP_TOKENis only consulted by the self-hosted claim path. - The
invite_onlysignup policy, and the two other self-hosted signup policies. - The public-app lock, which forces every application route to require a non-anonymous session.
What changes in a self-hosted build
Because the
/, /auth/sign-up and /auth/sign-in redirects stack, a fresh self-hosted deployment sends every visitor at / to /setup until the instance is claimed, and to /auth/sign-in afterwards.
RECEIPT_DEFAULT_FREE_SEAT_COUNT is a cloud-plan knob. It raises the free-plan seat ceiling that active members and pending invitations are counted against, and it is a positive integer that falls back to 5. It does not apply to a self-hosted instance, which is provisioned on the self_hosted plan with 100000 seats and usage metering off./pricing on a self-hosted build renders a static card instead of the pricing UI:
Self-hosted instance
Cloud billing and plan upgrades are disabled in self-hosted mode. This deployment already runs with the self-hosted capability profile.
The self_hosted plan id is only valid when the instance is self-hosted.
Telemetry
A self-hosted build returns no PostHog API key, so a self-hosted instance never sends anything to PostHog. The setup snapshot reports PostHog, Stripe billing, auth email and social sign-in with the statusnot_available_in_self_host.
Claiming the instance: the /setup wizard
/setup is a two-step wizard.
1
Hello Human
Subtitle:
Enter the setup token to claim this instance. The field is labelled Setup token, with placeholder Paste the token here and help text Use the token from your self-hosted setup.The token can also arrive in the URL as ?setupToken=, ?setup-token= or ?token=. It is read and then stripped from the URL.2
Create your admin account
Subtitle:
Finish setup by creating the first admin account. Fields: Name, Email, Password, Confirm password. The button reads Create account.Completing this step takes a Postgres advisory lock, creates the first admin user, marks the instance claimed, signs the admin in, and hard-navigates to /chat. If that final sign-in returns no session cookie, the wizard reports Failed to finish setup. and stays on the form.SELF_HOSTED_SETUP_TOKEN with a timing-safe comparison, and the claim runs under a named advisory lock, so two people racing the wizard cannot both create an admin.
When the instance is claimed, it is stamped with the signup policy invite_only and the public app locked.
Setup refusals
Setup token is required.The setup token is invalid.The setup token is not configured yet.Unable to verify the setup token.Self-hosted setup has already been completed.Setup is only available in self-hosted mode.Name is required.Enter a valid email address.Password must be at least 8 characters long.Passwords do not match.Failed to finish setup.
When
VITE_SELF_HOST_SOURCE=railway is set at build time, step one shows a distribution-specific subtitle and help text instead of the generic ones. VITE_SELF_HOST_SOURCE is otherwise just a label.Signup policies
Signup policy lives on a singleton instance-settings row. Three values exist:invite_only (the default set at claim time), shared_secret and open. The guard runs before signup:
The guard reads the setup token from the
x-receipt-setup-token header or a selfHostedSetupToken body field, and the shared secret from x-receipt-signup-secret or selfHostedSignupSecret. It checks the email address first, then the four states above in order.
Tokens and secrets are compared with a timing-safe comparison, and the shared secret is stored scrypt-hashed. Pending invitations are matched case-insensitively and must not have expired.
The readiness checklist
The setup wizard renders a checklist built from the environment the server actually sees. Required:
Optional: the markdown worker.
Reported as
not_available_in_self_host: PostHog, Stripe billing, auth email, social sign-in.
The
Auth email row is misleading. It is hard-coded to not_available_in_self_host, but a self-hosted instance with a mail transport configured does deliver invitation email. The copyable signup link is shown in the invite dialog either way.The self-host environment template
The repository ships a second, self-hosting-specific environment template atapps/start/.env.self-host.example, separate from the general apps/start/.env.example. Alongside VITE_APP_INSTANCE_MODE=self_hosted and SELF_HOSTED_SETUP_TOKEN it presets the choices a self-hosted deployment usually wants — RECEIPT_REPO_KEY, VITE_ENABLE_EMBEDDING=false, ALLOW_USER_COST_DISPLAY=true, VITE_DISABLE_REDIS=false and UPLOAD_STORAGE_PROVIDER=s3_compatible — plus the storage, Redis and model-access blocks. Configuration lists the variables the code actually reads.
No other document in the repository references the template — not the README, not the development guide, not the local setup guide. If you were looking for it, that is why you did not find it.
Next step: Set up Postgres, run the migrations, and check the sync publication.