Skip to main content

Environments and secrets

:::warning None of this is set up yet This repository has never been deployed. The workflows are written and the code is ready, but until the resources below exist and the secrets are set, preview.yml fails on every pull request and deploy.yml fails on every push to main.

This page is the checklist for making that stop. :::

Environments

localpreviewproduction
WorkerMiniflare on :8787crag-topo-api-previewcrag-topo-api
D1.wrangler/state on diskcrag-topo-db-previewcrag-topo-db
EAS channelpreviewproduction
Bundle idapp.cragtopo.ios.developmentapp.cragtopo.ios.previewapp.cragtopo.ios
IdentityLocal device id, trustedLocal device id, trusted501 unless ALLOW_UNAUTHENTICATED

The production row is deliberate. There are no accounts (ADR-0007) — the app mints a local usr_<ULID> and the server takes it at face value — so a production deployment refuses to serve the sync endpoints until someone explicitly sets ALLOW_UNAUTHENTICATED.

One-time Cloudflare setup

:::tip Authenticate with a token, not wrangler login wrangler login writes to ~/Library/Preferences/.wrangler/config/default.toml, which is machine-wide — it will replace whatever account you are logged into for every other project on that machine. If you use Cloudflare for anything else, that is not what you want.

Create an API token instead (the same one CI needs, see below) and export it:

export CLOUDFLARE_API_TOKEN=...
export CLOUDFLARE_ACCOUNT_ID=...

It takes precedence over the stored login and leaves it untouched. wrangler whoami will confirm which account you are actually about to create things in — worth checking before you do. :::

# Databases
wrangler d1 create crag-topo-db
wrangler d1 create crag-topo-db-preview

Both commands print a database_id. Put them into apps/api/wrangler.jsonc, replacing the 00000000-0000-0000-0000-000000000000 placeholders — there are three occurrences (top level, env.preview, env.production).

# Pages projects: the docs site and Storybook
wrangler pages project create crag-topo-docs --production-branch main
wrangler pages project create crag-topo-storybook --production-branch main

# First migration on both databases, and seed data on preview
pnpm --filter @crag-topo/api exec wrangler d1 migrations apply DB --env preview --remote
pnpm --filter @crag-topo/api exec wrangler d1 migrations apply DB --env production --remote
pnpm --filter @crag-topo/api exec wrangler d1 execute DB --env preview --remote \
--file=./seed/seed.sql

Bootstrap the preview Worker

This step is easy to miss and breaks the first PR. The preview workflow uses wrangler versions upload, which refuses to run against a Worker that does not exist yet:

You cannot upload a new version of a Worker that does not yet exist. Please run the deploy command first.

So deploy it once, by hand:

pnpm --filter @crag-topo/api exec wrangler deploy --env preview --var GIT_SHA:bootstrap

Production needs no equivalent — deploy.yml uses wrangler deploy, which creates the Worker on first run.

One-time Expo setup

pnpm --filter @crag-topo/mobile exec eas login
pnpm --filter @crag-topo/mobile exec eas init --force # creates the project, prints a project id

eas init cannot write to app.config.ts because it is a dynamic config — that is expected. The project id is committed there as a default instead; it is not a secret, and having it in the file means eas update works locally without exporting anything.

eas-cli is a pinned devDependency, so pnpm exec eas is the same binary here and in CI. Do not rely on a globally-installed one — the output shape of eas update --json differs between major versions, and the QR step in preview.yml parses it.

Then build the dev clients once. The preview profile is the one that matters — it is what lets a reviewer scan a PR's QR code:

pnpm --filter @crag-topo/mobile exec eas build --profile development --platform ios
pnpm --filter @crag-topo/mobile exec eas build --profile preview --platform ios

:::tip Address D1 by binding, not by name Every command above uses DB, the binding, rather than a database name. The name changes per environment — crag-topo-db-preview under --env preview — so a command with a name in it either resolves to the wrong database or fails outright. The binding is DB in all three environments. :::

Repository secrets

Settings → Secrets and variables → Actions → Secrets.

SecretWhatHow to get it
CLOUDFLARE_API_TOKENDeploys Workers, applies D1 migrations, deploys PagesCloudflare dashboard → My Profile → API Tokens → Create Token
CLOUDFLARE_ACCOUNT_IDWhich account to deploy intoCloudflare dashboard sidebar, or wrangler whoami
EXPO_TOKENPublishes EAS Updates from CIexpo.dev → Account settings → Access tokens

Cloudflare token permissions

Create a custom token with exactly these, and nothing more:

ScopePermissionLevel
AccountWorkers ScriptsEdit
AccountD1Edit
AccountCloudflare PagesEdit
AccountAccount SettingsRead

A global API key would also work and is a bad idea — it is not scoped and cannot be rotated independently.

Repository variables

Settings → Secrets and variables → Actions → Variables. These are not secret; they are here so they are not hard-coded in a workflow.

VariableExampleUsed by
EAS_PROJECT_IDa1b2c3d4-… — from eas initpreview, deploy
PRODUCTION_API_URLhttps://crag-topo-api.<subdomain>.workers.devdeploy (health check, app config)

GitHub environments

preview.yml and deploy.yml declare environment: preview and environment: production. Creating those under Settings → Environments is optional but recommended:

  • Scope secrets to an environment rather than the whole repository
  • Add a required reviewer on production, so a merge to main does not deploy unattended
  • Get a deployment history per environment

Local secrets

There are none. Local development needs no Cloudflare account, no Expo account, and no .dev.vars — the sync endpoints take a bare usr_<ULID> as the bearer token (ADR-0007).

If a future binding does need a local secret, apps/api/.dev.vars is gitignored and is where it goes.

Verifying the setup

Push a branch and open a pull request. A working setup produces a comment within a few minutes containing four links. Then:

curl -s <preview-url>/health

revision should equal your commit SHA and database should be ok.

If the comment never appears, check the Actions tab — the health gate fails the job rather than posting a link to something broken, which is intentional.

Rotating a secret

  1. Create the new one at the provider.
  2. Update the GitHub secret.
  3. Re-run the last workflow to confirm.
  4. Revoke the old one.

In that order. Revoking first breaks every open PR's preview.