Rights Institute Docs

Environment Variables

Every variable the app reads, and where to get it.

Copy .env.example to .env and fill these in.

VariablePurpose
GOOGLE_CLIENT_IDGoogle OAuth client ID — powers both the server OAuth flow and the Google One Tap prompt (read at request time, so a runtime var/secret works without rebuilding — see Authentication)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
BETTER_AUTH_SECRETAuth encryption secret — generate with openssl rand -base64 32
BETTER_AUTH_URLApp base URL used by better-auth (e.g. http://localhost:3000). Optional in production — left unset, better-auth derives the origin from each request, which is what you want for a Worker serving several hostnames
NEXT_PUBLIC_APP_URLPublic base URL, used as a fallback for BETTER_AUTH_URL
NEXT_PUBLIC_GOOGLE_CLIENT_IDOptional build-time copy of GOOGLE_CLIENT_ID. Only needed for deployments that prefer inlining it; GOOGLE_CLIENT_ID alone is enough on Cloudflare
TURSO_DATABASE_URLlibSQL/Turso database URL — used only outside Cloudflare (local dev, tests). The deployed Worker gets its database from the DB D1 binding instead
TURSO_AUTH_TOKENlibSQL/Turso auth token, paired with the above
AUTH_RESEND_KEYResend API key used to send magic-link sign-in emails
RESEND_API_KEYOptional separate Resend key for agreement invites and certificates; falls back to AUTH_RESEND_KEY
AGREEMENTS_EMAIL_FROMSender for agreement emails (default Rights Institute <noreply@rights.institute>)
SITE_URLOptional public origin used in emailed links (default: the request's own origin)
PROSPER_RPC_URLJSON-RPC endpoint for anchoring Prosper ledger blocks on-chain (optional — see Contract Builder)
PROSPER_REGISTRY_ADDRESSDeployed AgreementRegistry contract (packages/prosper-coin/contracts)
PROSPER_ANCHOR_KEYPrivate key of an account allowed to call anchor() — set as a secret
PROSPER_CHAIN_IDOptional chain id; read from the RPC when unset
PROSPER_EXPLORER_URLOptional block explorer base URL for linking anchor transactions

Google OAuth credentials

Get GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET from the Google Cloud Console.

Add these as authorized redirect URIs on the OAuth client:

  • Local: http://localhost:3000/api/auth/callback/google
  • Production: https://rights.institute/api/auth/callback/google

For Google One Tap, also add your local and production origins (e.g. http://localhost:3000, https://rights.institute) as Authorized JavaScript origins — One Tap validates the origin the prompt is served from, separately from the OAuth redirect URIs.

The database is a binding, not a variable

On Cloudflare the app reads the DB D1 binding declared in wrangler.jsonc. A binding is part of the deployed Worker's own configuration, so — unlike a connection string kept in a dashboard variable — it cannot go missing between deploys, and no TURSO_* value is needed in production at all.

Apply the migrations (they live beside the drizzle schema, in lib/db/drizzle) before the first sign-in, or every auth request fails on a missing user table:

wrangler d1 migrations apply DB --remote

TURSO_DATABASE_URL is still the fallback for anything that is not a Worker, which is how local dev and the tests reach a database.

Setting these on Cloudflare

The deployed Worker does not read your local .env. Set each value as a Worker secret (or as a var in the Cloudflare dashboard) and redeploy:

wrangler secret put BETTER_AUTH_SECRET
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put AUTH_RESEND_KEY

Everything is read per request through @/lib/env's getEnv() — the Cloudflare runtime env first, then process.env — so a rotated secret takes effect without a rebuild.

wrangler.jsonc sets keep_vars: true. Without it, every wrangler deploy deletes plaintext Variables typed into the Cloudflare dashboard, because this config declares no vars of its own — so a deploy would quietly empty GOOGLE_CLIENT_ID and leave /api/auth/* answering 503. Secrets survive either way; the flag protects dashboard-entered variables.

Checking what the deployment actually sees

GET /api/health reports which configuration groups the running instance can read (presence only, never values):

curl -s https://rights.institute/api/health
{
  "status": "degraded",
  "config": { "auth": true, "database": true, "google": true, "magicLinkEmail": false },
  "missing": [],
  "warnings": ["BETTER_AUTH_SECRET"]
}
  • missing lists what auth genuinely cannot run without — only the database qualifies, and status is then error. Everything in missing answers /api/auth/* with a 503.
  • warnings lists configuration that should be set but that auth serves requests without; status is degraded. Sign-in still works.

This is the first thing to check when /api/auth/get-session misbehaves — see Authentication for what each absent value disables.

If the browser console warns [auth] Google One Tap skipped — no Google client ID, ask the running deployment what it can see:

curl -s https://rights.institute/api/client-config
# {"googleClientId":"…apps.googleusercontent.com"}

An empty value means GOOGLE_CLIENT_ID isn't set as a Worker var/secret.

Off Cloudflare — local dev included — TURSO_DATABASE_URL is required, because there is no DB binding to fall back to. The app uses the libSQL web client (the only one that runs on Cloudflare Workers), which supports libsql:/https:/wss: URLs only — there is no local file: database fallback.