One-time setup steps for every variable in .env.example, grouped into the same sections as that file. That file itself stays a clean list of names — this doc is where the "how do I actually get one of these" detail lives.
Every integration here is optional and degrades gracefully: the site runs fine with an empty .env, just with the affected widget/page showing a "not connected" state instead of data.
Required in production. adapter-node needs this to build absolute URLs and validate incoming request origins when running behind a reverse proxy (Coolify's Traefik) — without it, adapter-node falls back to guessing from request headers, which can misbehave behind a proxy. Set in Coolify's environment variables UI, not in a committed .env. Example: https://razerghost.xyz.
Signs the session cookie (session.ts) via HMAC — not a GitHub value. Generate a random secret once:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Rotating this value invalidates all existing sessions.
Extended streaming history exports can be tens of MB across many files. adapter-node's default request body limit (512kb) will reject uploads above that at /spotify-import — raise it in Coolify's environment UI if your export is large. Accepts a byte count with an optional K/M/G suffix (e.g. 200M) — not 0, which SvelteKit treats as a 0-byte limit rather than "unlimited". Use the literal string Infinity to fully disable the limit. See SvelteKit's adapter-node docs.
Restricted to a single GitHub account — the one matching site.githubUsername in config.ts. Any other GitHub account is explicitly rejected at /auth/callback; no session is issued.
- Create an OAuth App at https://github.com/settings/developers
- Homepage URL: your production URL
- Authorization callback URL:
<your production URL>/auth/callback - Register a second OAuth App for local dev, with callback URL
http://localhost:5173/auth/callback— GitHub requires an exact match and one OAuth App can't have two callback URLs.
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRETcome from that app.
See auth.md for the full login flow.
These same two values are also reused (as Basic Auth, not part of the login flow) to raise the GitHub REST API's unauthenticated rate limit from 60/hour to 5,000/hour for the GitHub activity/repo-stats widgets — see integrations.md. If they're unset, those widgets still work, just capped at the lower unauthenticated limit.
Powers the recently-played history section of SpotifyWidget.svelte and /listens live scrobbling — not the live "now playing" track itself, which comes from Lanyard/Discord presence instead (see integrations.md). Shows "Spotify not connected" (recently-played section hidden) if unset.
-
Create an app at https://developer.spotify.com/dashboard.
-
SPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRETcome from that app. -
SPOTIFY_REFRESH_TOKENrequires a one-time OAuth authorization-code flow with theuser-read-currently-playing user-read-recently-playedscopes, exchanged for a refresh token — a manual step done once, not part of the app itself. The recently-played history section of the widget silently hides itself if the token only hasuser-read-currently-playing.scripts/spotify-refresh-token.mjs does the whole flow: it serves the callback on
http://127.0.0.1:8888/callback(register that exact URI on the Spotify app first — Spotify rejectslocalhost, and loopback-IP URIs are the documented exception to its HTTPS-only rule), prints an authorize URL to open, and prints the resulting token plus its granted scopes.node scripts/spotify-refresh-token.mjs <client_id> <client_secret>
See integrations.md for how the token is used at runtime.
Powers /listens, built from Spotify's own "extended streaming history" data export (request it at https://support.spotify.com/us/article/understanding-your-data/, can take up to 30 days to arrive), not the live API. Upload the export's JSON files at /spotify-import, gated behind the same GitHub login as /admin — no separate credentials needed.
Optional — path to the SQLite file backing this page (spotify-history-db.ts). Defaults to ./data/spotify-history.db if unset. This file is not trivially rebuildable if lost: the export is a one-time historical dump, so make sure this points inside the same persistent Coolify volume as the other *_DB_PATH vars.
See listens.md for the import/parsing details.
Optional — enables live scrobbling into spotify-history.db between manual exports (+server.ts). Requires the Spotify vars above with the user-read-recently-played scope.
Nothing issues this value — it's a shared secret you invent, like SESSION_SECRET and BACKUP_SECRET, and the only requirement is that the scheduler sends the same string this var holds:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
If set, hit GET /api/spotify/scrobble with an Authorization: Bearer <this value> header on a schedule (every 15-30 min — Spotify's recently-played endpoint only returns the last 50 plays, so longer gaps lose history). From a Coolify Scheduled Task, which runs inside the container (no curl there), that's:
node scripts/hit-endpoint.mjs /api/spotify/scrobble SPOTIFY_SCROBBLE_SECRET
An external scheduler hits the public URL with curl instead. ?secret=<this value> also works, but the header is preferred — query strings tend to end up in proxy/access logs. Leave unset to disable the endpoint (it 503s without this).
Powers /watchlist (simkl.ts). Shows "Simkl not connected" if unset. Uses Simkl's AUTH V2: access tokens only last 7 days, so the app keeps just the refresh token in env and mints access tokens from it itself (simkl-auth.ts).
- Register an AUTH V2 app at https://simkl.com/settings/developer/ with the app type TV, devices & command line (no client secret, no redirect URL). Its
client_idis public and safe to commit; Ghostbase's is767808ee648e75daad56ea303fa9a3379212740a8f119d20b6a468a5d8288ecc. - Get
SIMKL_REFRESH_TOKENvia the device (PIN) flow:Go to simkl.com/pin, enter the code it shows, approve, and it printsnode scripts/simkl-token.mjs <client_id>SIMKL_REFRESH_TOKEN=.... It asks for no scope, so the token is read-only (media:read), which is all this page needs.
Things to know about the refresh token:
- Never commit it anywhere, including the backup repo. Simkl automatically revokes tokens it finds on GitHub, private repos included, and revoking either token kills the whole grant. That's why the access token is only held in memory, not in
data/(which backups push to git). - Use one grant per environment. Refreshing invalidates the previous access token right away, so prod and a local
pnpm devsharing one refresh token would keep cutting each other off. Run the script once for Coolify and again for your local.env. - It lasts 180 days, and the window resets on every refresh. The 24h background loop (simkl-refresh.ts) refreshes about once a week, so it only expires if the app is down for six months. It also dies if you revoke the app under Simkl's Connected Apps. Either way the logs show
Simkl token refresh failed: 400 invalid_grant,/watchlistkeeps serving its last snapshot, and the fix is to re-run the script.
Legacy SIMKL_ACCESS_TOKEN: the old AUTH V1 long-lived token (V1 PIN flow, 5-year expiry) is still honoured when SIMKL_REFRESH_TOKEN is unset, so existing deployments keep working. Simkl retires V1 around April 2027. It must be paired with the old V1 SIMKL_CLIENT_ID; a V1 token doesn't work with a V2 client ID and can't be converted. Once SIMKL_REFRESH_TOKEN is set, remove SIMKL_ACCESS_TOKEN.
See watchlist.md for caching/enrichment details.
Optional — path to the SQLite cache backing the Watching page's genre/synopsis enrichment (simkl-cache.ts). Defaults to ./data/simkl-cache.db if unset. Losing this file isn't destructive — it just goes cold and re-warms itself over the next several page loads — but pointing it at the same Coolify persistent volume as the other *_DB_PATH vars (i.e. leaving them all unset, so they share ./data) avoids that cold start on every redeploy.
Optional, directory holding images uploaded through the /admin/media library (media.ts), used for devlog/project cover images, galleries, and post bodies. Defaults to ./data/media if unset. Files here are served publicly (no login required) since they appear on public devlog/project pages — only uploading and deleting are admin-gated. Point this at the same persistent Coolify volume as the other data/* paths — losing it breaks any published post/project referencing an uploaded image.
Optional — path to the SQLite file backing the homepage's "Right now" status card, edited at /admin/status (status-db.ts). Defaults to ./data/status.db if unset. Losing this is non-destructive — it just reverts to the hardcoded fallback in status-db.ts.
BACKUP_SECRET / BACKUP_GIT_REMOTE / BACKUP_GIT_BRANCH / BACKUP_GIT_USER_NAME / BACKUP_GIT_USER_EMAIL
Backs up the SQLite DBs to a private git repo. See backups.md for full one-time setup (creating the backup repo, minting a scoped PAT, scheduling the endpoint) — that doc covers this in more depth than fits here.