Skip to content

Environment Variables

swayam25 edited this page Aug 26, 2026 · 3 revisions

Three files, one per concern. All three are gitignored, so copy them from the .example next to each one.

cp apps/server/.env.example apps/server/.env
cp apps/server/.env.server-db.example apps/server/.env.server-db
cp apps/web/.env.example apps/web/.env

apps/server/.env

Everything the API needs. A missing required key throws on boot with the name of the offender.

Key Type Required Description
JPA_AUTH_SECRET str Yes Signing secret for better-auth sessions.
JPA_AUTH_URL str Yes Where the API itself is reachable. http://localhost:3000 locally.
JPA_APP_URL str Yes Where the web app is reachable, used for CORS and auth redirects.
JPA_DATABASE_URL str Yes Postgres connection string. The default already points at the docker container.
JPA_REDIS_URL str Yes Redis connection string, backs rate limiting and caching.
PUBLIC_COC_API_BASE_URI str Yes Clash of Clans API base. Defaults to the RoyaleAPI proxy, so no static IP needed.
JPA_COC_API_TOKEN str Yes Clash of Clans API token, from the developer portal.
JPA_DISCORD_ID str Yes Discord OAuth application ID, used for sign in.
JPA_DISCORD_SECRET str Yes Discord OAuth client secret.
JPA_DISCORD_BOT_TOKEN str Yes Bot token, used to read guild membership and roles.
JPA_TURNSTILE_SECRET_KEY str Yes Cloudflare Turnstile secret, the server side of the captcha.
SENTRY_DSN str No Sentry project DSN. Leave empty locally.
SENTRY_SPOTLIGHT str No 1 sends errors to the local Spotlight container instead of Sentry.
PORT int No Port the API listens on. Defaults to 3000.

Tip

Generate the auth secret with:

openssl rand -base64 32

Caution

SENTRY_SPOTLIGHT=1 makes the server drop its Sentry DSN entirely and route errors to a local Spotlight container, see instrument.ts. That container only exists under the all and analytics Compose profiles. In production it must be 0, otherwise every server-side error is silently discarded despite a valid DSN being configured.

apps/server/.env.server-db

Read by the docker containers and by the API, so both sides stay in sync. The defaults work as is.

Key Type Required Description
POSTGRES_USER str Yes Superuser the Postgres container creates on first boot.
POSTGRES_PASSWORD str Yes Its password. Has to match whatever is in JPA_DATABASE_URL.
POSTGRES_DB str Yes Database name created on first boot.
MINIO_ROOT_USER str Yes MinIO access key, doubles as the console login.
MINIO_ROOT_PASSWORD str Yes MinIO secret key.
MINIO_ENDPOINT str Yes Where the API talks to MinIO, the S3 API on 7105.
MINIO_PUBLIC_URL str Yes Base URL baked into uploaded file links. Same as the endpoint locally.

apps/web/.env

Everything here is PUBLIC_, meaning SvelteKit ships it to the browser.

Key Type Required Description
PUBLIC_SERVER_URL str Yes Base URL of the API the browser calls.
PUBLIC_TURNSTILE_SITE_KEY str Yes Turnstile site key, the public half of JPA_TURNSTILE_SECRET_KEY.
PUBLIC_SENTRY_DSN str No Browser Sentry DSN. A different project from the server's.
PUBLIC_SENTRY_SPOTLIGHT str No 1 routes browser errors to the local Spotlight container.

Warning

Never put a secret in a PUBLIC_ variable. Vite inlines them into the bundle at build time, so they ship to every visitor. And editing them on a running container changes nothing, you have to rebuild.

Production values

Only the values change in production, because services address each other by container name over the Docker network instead of localhost.

Key Production value Why
JPA_DATABASE_URL postgresql://<user>:<pass>@jpa-db:5432/jpa Container name, not localhost:7101
JPA_REDIS_URL redis://default@jpa-redis:6379 Same
JPA_AUTH_URL https://api.clashwithjpa.com Public origin of the API, used for CORS and the OAuth callback
JPA_APP_URL https://clashwithjpa.com Public origin of the site
MINIO_ENDPOINT http://jpa-minio:9000 Server → MinIO, stays inside the network
MINIO_PUBLIC_URL https://cdn.clashwithjpa.com Browser → MinIO, baked into every uploaded file's URL
PUBLIC_SERVER_URL https://api.clashwithjpa.com Where the browser calls the API
SENTRY_SPOTLIGHT 0 Spotlight is a local-only debug container and is not in the prod profile

Generate a fresh JPA_AUTH_SECRET for the server, never reuse the local one.

Creating your own Discord application

Everything comes from the Discord Developer Portal. Create a New Application, then:

Where What to grab Goes into
General Information Application ID JPA_DISCORD_ID
OAuth2 → Client Secret Reset Secret, copy it JPA_DISCORD_SECRET
Bot → Token Reset Token, copy it JPA_DISCORD_BOT_TOKEN

Two settings also need changing:

  1. OAuth2 → Redirects, add http://localhost:3000/api/auth/callback/discord for local work, and https://api.clashwithjpa.com/api/auth/callback/discord for production. Sign in fails with invalid_redirect_uri without it.
  2. Bot → Privileged Gateway Intents, turn on Server Members Intent. The API lists guild members through discord.ts, and Discord rejects that endpoint without the intent.

Then invite the bot to a test guild you control, since it reads that guild's roles, channels and members. The OAuth scopes the app requests are identify, email, guilds and guilds.members.read.

Getting Turnstile keys

The quickest path for local work is Cloudflare's test keys, which need no account and always pass:

# apps/web/.env
PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA

# apps/server/.env
JPA_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

They only validate against each other, so both have to be the test pair. For real keys, go to the Cloudflare dashboardTurnstileAdd widget, add your hostname, and take the Site Key and Secret Key from there. Production needs a real pair scoped to the live hostname.

Turnstile Dashboard

Clone this wiki locally