Environment Variables
Every variable the app reads, and where to get it.
Copy .env.example to .env and fill these in.
| Variable | Purpose |
|---|---|
GOOGLE_CLIENT_ID | Google 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_SECRET | Google OAuth client secret |
BETTER_AUTH_SECRET | Auth encryption secret — generate with openssl rand -base64 32 |
BETTER_AUTH_URL | App 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_URL | Public base URL, used as a fallback for BETTER_AUTH_URL |
NEXT_PUBLIC_GOOGLE_CLIENT_ID | Optional 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_URL | libSQL/Turso database URL — used only outside Cloudflare (local dev, tests). The deployed Worker gets its database from the DB D1 binding instead |
TURSO_AUTH_TOKEN | libSQL/Turso auth token, paired with the above |
AUTH_RESEND_KEY | Resend API key used to send magic-link sign-in emails |
RESEND_API_KEY | Optional separate Resend key for agreement invites and certificates; falls back to AUTH_RESEND_KEY |
AGREEMENTS_EMAIL_FROM | Sender for agreement emails (default Rights Institute <noreply@rights.institute>) |
SITE_URL | Optional public origin used in emailed links (default: the request's own origin) |
PROSPER_RPC_URL | JSON-RPC endpoint for anchoring Prosper ledger blocks on-chain (optional — see Contract Builder) |
PROSPER_REGISTRY_ADDRESS | Deployed AgreementRegistry contract (packages/prosper-coin/contracts) |
PROSPER_ANCHOR_KEY | Private key of an account allowed to call anchor() — set as a secret |
PROSPER_CHAIN_ID | Optional chain id; read from the RPC when unset |
PROSPER_EXPLORER_URL | Optional 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 --remoteTURSO_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_KEYEverything 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"]
}missinglists what auth genuinely cannot run without — only the database qualifies, andstatusis thenerror. Everything inmissinganswers/api/auth/*with a 503.warningslists configuration that should be set but that auth serves requests without;statusisdegraded. 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.