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
local | preview | production | |
|---|---|---|---|
| Worker | Miniflare on :8787 | crag-topo-api-preview | crag-topo-api |
| D1 | .wrangler/state on disk | crag-topo-db-preview | crag-topo-db |
| EAS channel | — | preview | production |
| Bundle id | app.cragtopo.ios.development | app.cragtopo.ios.preview | app.cragtopo.ios |
| Identity | Local device id, trusted | Local device id, trusted | 501 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
deploycommand 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.
| Secret | What | How to get it |
|---|---|---|
CLOUDFLARE_API_TOKEN | Deploys Workers, applies D1 migrations, deploys Pages | Cloudflare dashboard → My Profile → API Tokens → Create Token |
CLOUDFLARE_ACCOUNT_ID | Which account to deploy into | Cloudflare dashboard sidebar, or wrangler whoami |
EXPO_TOKEN | Publishes EAS Updates from CI | expo.dev → Account settings → Access tokens |
Cloudflare token permissions
Create a custom token with exactly these, and nothing more:
| Scope | Permission | Level |
|---|---|---|
| Account | Workers Scripts | Edit |
| Account | D1 | Edit |
| Account | Cloudflare Pages | Edit |
| Account | Account Settings | Read |
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.
| Variable | Example | Used by |
|---|---|---|
EAS_PROJECT_ID | a1b2c3d4-… — from eas init | preview, deploy |
PRODUCTION_API_URL | https://crag-topo-api.<subdomain>.workers.dev | deploy (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
- Create the new one at the provider.
- Update the GitHub secret.
- Re-run the last workflow to confirm.
- Revoke the old one.
In that order. Revoking first breaks every open PR's preview.