-
Notifications
You must be signed in to change notification settings - Fork 1
Environment Variables
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/.envEverything 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 32Caution
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.
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. |
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.
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.
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:
-
OAuth2 → Redirects, add
http://localhost:3000/api/auth/callback/discordfor local work, andhttps://api.clashwithjpa.com/api/auth/callback/discordfor production. Sign in fails withinvalid_redirect_uriwithout it. - 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.
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=1x0000000000000000000000000000000AAThey only validate against each other, so both have to be the test pair. For real keys, go to the Cloudflare dashboard → Turnstile → Add widget, add your hostname, and take the Site Key and Secret Key from there. Production needs a real pair scoped to the live hostname.
