Skip to content
Club system guide

Deployments and checks

How each repo is tested and deployed, where secrets are stored, and what health checks tell us.

Each repository tests and deploys its own code. Changing the wiki does not deploy the API. Fixing email does not rebuild the website.

When a change affects more than one service, both sides must work while they are deployed at different times.

System flow

How each service deploys

Each repository runs its own checks. Most services deploy from main to Cloudflare. The VPS Gateway deploys through Git and systemd.

Deploy a Cloudflare Worker

  1. Push to main Starts the website, API, Discord, email, or scheduler workflow
  2. Run checks Checks types and the build, plus tests where configured
  3. Deploy Wrangler publishes the Worker
  4. Logs and traces Uses 10% head sampling

Deploy the API

  1. D1 migrations Applies database changes first
  2. Bootstrap check Leaves out the Discord binding when needed
  3. API deploy Publishes the current routes and handlers

Deploy the wiki

  1. Push to main Starts the static Astro build
  2. Astro + Pagefind Builds the HTML and search index
  3. Cloudflare Pages Publishes the dist folder

Deploy the VPS Gateway

  1. Build locally Builds the Gateway and packages the dist folder
  2. Push the deploy repo Sends the built files to the bare Git repository
  3. Post-receive hook Checks out the files and installs production dependencies
  4. Restart systemd Replaces the running Gateway process
D1 migrations run before the API Worker deploys. When the Discord Worker does not exist yet, the bootstrap setup leaves out its binding.
RepositoryChecksDeploys toExtra step
websiteTests, Astro build, TypeScript, React Doctor, deploy artifact checkCloudflare WorkerUses the Astro Cloudflare server entrypoint and API service binding.
apiTests, TypeScript, dry-run Worker build, React DoctorCloudflare WorkerApplies D1 migrations before the normal deploy.
discordWorker tests, TypeScript, ESLint, Worker and Gateway buildsCloudflare WorkerThis workflow does not deploy the VPS Gateway.
email-workerParser tests, TypeScript, dry-run Worker build, React DoctorCloudflare WorkerKeeps the five-minute receipt retry Cron and KV binding.
scheduler-workerTypeScript, dry-run Worker build, React DoctorCloudflare WorkerKeeps the thirty-minute Cron and API binding.
wikiContent/UI tests and Astro static buildCloudflare PagesBuilds Pagefind search data into the static output.

The Worker workflows use Node 22. The wiki Pages workflow uses Node 24. Each Wrangler file sets its own runtime compatibility date and flags. The CI Node version does not set them.

Most repositories follow these steps:

  1. Open or update a pull request.
  2. Install the locked dependencies.
  3. Run the repository checks.
  4. Build the same files that Wrangler or Pages will publish.
  5. After a successful push to main, sign in to Cloudflare and deploy.

Some pipelines also audit dependencies and check for secrets. These checks can catch common mistakes, but reviewers must still check authorization, input validation, and new trust boundaries.

The normal API deployment command applies remote D1 migrations before it publishes the new Worker.

This gives new code the tables and columns it needs. It also means the migration must work with the old Worker until the new Worker is live.

For a safe schema change:

  • Add a new column or table before code needs it.
  • Do not remove or rename data while old code may still use it.
  • Keep API responses compatible with the website and Discord Worker during the rollout.
  • Make migrations safe to run again.

The API normally binds to the Discord Worker. In a new environment, the API expects the ppc-bot service, but the Discord Worker also expects the API. Neither service exists yet.

The API has a bootstrap deployment that creates a temporary Wrangler file without the Discord binding. This lets the API deploy first. The Discord Worker deploys next, and then the API deploys normally with the binding restored.

Use bootstrap only to create an environment. It is not the normal production setup.

The bootstrap setup cannot send Discord events. Check that the normal binding is restored after both services exist.

The Discord repository builds the Worker and the Gateway. Its Cloudflare workflow deploys only the Worker.

The Gateway uses these steps:

  1. Build the Gateway locally and copy its compiled dist folder into a small deployment repository.
  2. Push that repository to the bare Git repository on the VPS.
  3. The post-receive hook checks out the built files.
  4. The hook installs production dependencies and restarts the systemd service.
  5. The discord.js process reconnects to Discord.

A successful Worker deployment does not prove that the VPS process, websocket, forwarding allowlist, Tunnel/VPC route, or systemd service works.

A healthy Gateway can also send an event that the Worker rejects because its secret, timestamp, nonce, or event contract is wrong.

Public settings can stay in the repository. Secrets cannot.

Stored in Git

Service names, D1, R2 and KV bindings, rate-limit namespaces, Cron expressions, routes, compatibility dates, and other public settings.

Stored as secrets

Auth secrets, OAuth credentials, internal tokens, signing secrets, Discord tokens, Resend keys, Turnstile keys, and external API credentials.

Set when deployed

Service bindings, custom domains, VPC routes, Email Routing, and VPS environment values that connect the services.

Secrets are set through Wrangler or the server environment. They are never hardcoded in TypeScript. Shared values must match in both services, but must never appear in a public variable or browser bundle.

A feature may cross several service boundaries. Check each boundary it uses.

  • Is untrusted form or query input validated?
  • Is the control accessible?
  • Does the UI show and enforce the same limit as the API?
  • Does any client code accidentally receive an internal token or secret?
  • Does the bridge strip spoofable internal headers?
  • Does the bridge leave Better Auth routes unversioned?
  • Does the API check authorization instead of trusting the UI?
  • Are queries parameterized?
  • Does the API check file limits before writing to R2?
  • If part of a storage write fails, does the code undo the completed part?
  • Is the event signed with a timestamp and fresh nonce?
  • Does the Worker reject event types it does not know?
  • Can a retry repeat the visible change?
  • Are the sender, recipient, message size, attachments, and parsed fields limited?
  • Do KV and D1 prevent duplicate delivery and fulfillment?
  • Is each failure marked as retryable or final?

All five Worker services enable Cloudflare observability with a 10% head-sampling rate. The API and Discord Worker use structured logs that hide common credential fields.

Useful log fields include:

  • Health status and service name
  • Request path, safe request or event ID, and duration
  • Scheduled job results for each path
  • Discord event name, nonce result, and REST status
  • Receipt intake key, retry type, and fulfillment result
  • Gateway connection, forwarding filter, and presence status

Sampling means that not every request gets a trace. Error logs should include enough limited context to investigate, but never cookies, authorization headers, raw secrets, full receipt bodies, or private user data.

There is no single trace that follows a request through every service. To investigate a problem, you may need to match timestamps and safe IDs across Cloudflare and VPS logs.

A health endpoint tests one service, not the full flow. It does not test every dependency or allowlist.

For example:

  • The API can be healthy while its Discord binding fails.
  • The Discord Worker can be healthy but receive no Gateway events because a channel is missing from the VPS allowlist.
  • The Gateway can be healthy while presence updates fail because the VPC route or signing secret is wrong.
  • The Email Worker can be healthy but receive no messages because an Email Routing rule changed.
  • A Cron trigger can succeed while one of its seven jobs fails.

Test the same path that the user action takes.

The code and Wrangler files show the planned setup. They do not show live secret values, Email Routing rules, the VPS environment, Tunnel/VPC state, or domain changes made outside Git.

Check the live resource when you need those details. A route in the repository does not prove that the deployed route works.

Expanded photograph