Skip to content

Check a deployment

deploy:check visits a deployment the way a customer does and tells you what is wrong with it. It works on any host, and on bun run production:up.

Terminal window
bun run deploy:check https://app.example.com --site https://example.com --email you@example.com
Terminal window
bun run deploy:check --before railway
bun run deploy:check --before aws

It reads .env.railway or .env.aws and holds it to the rules the API checks when it starts, so a wrong value stops you here instead of a new version that never starts. deploy:apply and deploy:apply:aws run it first and stop on a FAIL. A setting left empty is only a note: it may already be set on the host. A name nothing reads is a note too, since it is usually a typo.

Check It passes when
The API answers and reaches the database /api/health says ok
Requests are given an id The answer has an x-request-id header. See Logs
The API says how people sign in /api/config answers. It prints “by email” or the providers that are on
Strangers are turned away /api/me answers 401 without a session
The app loads / is a page with its script
Pages come with protective headers Strict-Transport-Security, X-Content-Type-Options and X-Frame-Options are set
Browsers may keep its files The script is served as immutable
A missing file is a 404 /assets/not-there.js is not answered with the app’s page
A link into the app loads on its own /sign-in is a page, not a 404
Billing refuses an unsigned webhook An unsigned webhook gets 400
It is served over HTTPS The address is https://, and http:// is sent on to it
Stand-ins are off STAND_INS is not set: Stripe and the model are the real ones

With --site, also:

Check It passes when
The site loads / answers 200
A page that does not exist says so /not-there answers 404
The sitemap has the site’s address Otherwise SITE_URL was wrong when the image was built
The contact form reaches the API An empty message is refused by the API, through the site
The site is served over HTTPS As above

With --email, it asks the app to send a sign-in link to that address. That is the one thing it changes: everything else is a request that is read or refused. Check that the email arrives.

Checking https://app.example.com
ok The API answers and reaches the database
ok Requests are given an id
ok The API tells the app how people sign in: with google and github
note Billing is off: no payment provider key is set
FAIL It is served over HTTPS: http://app.example.com answered 200 instead of sending people on
9 passed, 1 failed

FAIL names what is wrong. note is something to know that may be what you want: billing or the contact form is off, demo mode is on, or HTTPS was not checked because the address is your own machine.

The checks run at the same time, and one that cannot be made at all, such as an address that does not resolve, is a FAIL with the reason. It exits with 1 if anything failed and 2 if the arguments are wrong, so you can run it after every release in a script.

When production has to guarantee something new, add it to packages/deploy/src/check.ts, with a test beside it in check.test.ts. CI runs the same command against the production images on every pull request, so the check also guards your next change.