LinkDen
Self-Hosting

Environment Variables

Complete reference for all LinkDen environment variables.

Environment Variables

A quick-reference for every environment variable used by LinkDen.

Quick Start

cp .env.example .env

LinkDen uses a single .env file at the monorepo root. Five variables are required to run the app; a few more are needed to deploy it.

Required Variables

VariableDescriptionExample
BETTER_AUTH_SECRETSecret key for session signing. Generate with openssl rand -base64 32k3jf8s...
BETTER_AUTH_URLBase URL of the API serverhttp://localhost:3000
CORS_ORIGINFrontend URL (must match exactly, including protocol, no trailing slash)http://localhost:3001
NEXT_PUBLIC_SERVER_URLPublic URL of the API server, used by the Next.js frontendhttp://localhost:3000
NEXT_PUBLIC_SITE_URLPublic URL of the site itself. Used for robots.txt, sitemap.xml, and Open Graph URLs. Required for a production build and deploy (the deploy fails early when it is empty); only local dev falls back to http://localhost:3001http://localhost:3001

In production these are all the same hostname. LinkDen runs same-origin: the web Worker owns SITE_DOMAIN and the API Worker is routed under it at /api/* and /trpc/*. So with SITE_DOMAIN=links.yourdomain.com:

BETTER_AUTH_URL=https://links.yourdomain.com
NEXT_PUBLIC_SERVER_URL=https://links.yourdomain.com
CORS_ORIGIN=https://links.yourdomain.com
NEXT_PUBLIC_SITE_URL=https://links.yourdomain.com

Locally the web app (:3001) and API (:3000) are different origins, which is why the local examples differ.

Cloudflare Deployment

Only needed when deploying with bun ship or the GitHub Actions deploy workflow:

VariableDescription
CLOUDFLARE_API_TOKENAPI token with Workers Scripts:Edit, Workers Routes:Edit, D1:Edit, R2:Edit, Zone:Read, DNS:Edit, SSL and Certificates:Edit. The Zone permissions are only exercised when SITE_DOMAIN is set
CLOUDFLARE_ACCOUNT_IDYour Cloudflare account ID
ALCHEMY_PASSWORDEncrypts secrets inside Alchemy's deploy state. Generate with openssl rand -base64 32 and never change it once deployed
ALCHEMY_STATE_TOKENAuth token for the alchemy-state CloudflareStateStore Worker. The one LinkDen is configured for is MrDemonWolf's; third-party self-hosters deploy their own state worker or switch stateStore in packages/infra/alchemy.run.ts. See the Cloudflare guide
ALCHEMY_STAGEAlchemy stage name. Defaults to prod. Any other value (for example dev) deploys a second, independent copy whose resources carry the stage as a suffix (linkden-db-dev, linkden-images-dev, linkden-api-dev, linkden-dev) so it can never adopt, overwrite, or destroy the production ones
LINKDEN_DB_NAME / LINKDEN_IMAGES_BUCKETUpgrades only. Deploys made before 0.5.0 let Alchemy generate the D1/R2 names (a CI deploy produced linkden-database-runner and linkden-images-runner). Set these to those existing names so the prod stage adopts your data instead of creating an empty linkden-db / linkden-images. See upgrading an older deploy
SITE_DOMAINYour hostname, for example links.yourdomain.com. When set, Alchemy attaches a Custom Domain to the web Worker and /api/* + /trpc/* routes to the API Worker. Leave unset to deploy to workers.dev (staging; admin login does not work there)

Runtime Configuration

Most settings that were previously environment variables are now managed through the admin UI at /admin/settings and stored in the database:

SettingLocation
Email provider and API keySettings > Email
CAPTCHA provider and keysSettings > CAPTCHA
Apple Wallet Team ID, Pass Type IDSettings > Apple Wallet
Branding / whitelabelSettings > Branding
Contact form delivery modeSettings > Contact Form

This keeps the .env file minimal and lets you change settings without redeploying.

Apple Wallet (Optional)

Only required if you want to generate Apple Wallet passes. All five variables are needed together — if any one is missing, Wallet pass generation is disabled and the admin UI shows an incomplete status.

VariableDescription
WALLET_SIGNER_CERTYour Pass Type signing certificate (Base64-encoded PEM)
WALLET_SIGNER_KEYPrivate key for the signing certificate (Base64-encoded PEM)
WALLET_WWDR_CERTApple's WWDR certificate (Base64-encoded PEM)
WALLET_TEAM_IDYour Apple Developer Team ID (e.g. ABC12DEF34)
WALLET_PASS_TYPE_IDYour Pass Type Identifier (e.g. pass.com.yourname.linkden)

These are secrets — keep them in .env locally or in GitHub environment secrets. Never commit them to git.

See the Apple Wallet guide for step-by-step certificate setup.

Complete .env Example

# Required
BETTER_AUTH_SECRET=your-generated-secret-key
BETTER_AUTH_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:3001
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
NEXT_PUBLIC_SITE_URL=http://localhost:3001

# Cloudflare deployment only
# CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
# CLOUDFLARE_ACCOUNT_ID=your-cloudflare-account-id
# ALCHEMY_PASSWORD=your-generated-state-secret
# ALCHEMY_STATE_TOKEN=your-state-worker-token
# ALCHEMY_STAGE=prod
# SITE_DOMAIN=links.yourdomain.com

# Apple Wallet (optional — all 5 required together)
# WALLET_SIGNER_CERT=base64encodedcert...
# WALLET_SIGNER_KEY=base64encodedkey...
# WALLET_WWDR_CERT=base64encodedwwdr...
# WALLET_TEAM_ID=ABC12DEF34
# WALLET_PASS_TYPE_ID=pass.com.yourname.linkden

Setting Variables

Alchemy binds these to the Workers at deploy time from your .env (local bun ship) or from the GitHub production environment (deploy.yml). In GitHub, store SITE_DOMAIN and NEXT_PUBLIC_SITE_URL as variables and everything else as secrets. Values set by hand in the Cloudflare dashboard are overwritten on the next deploy.

First-Time Setup

LinkDen does not use environment variables or tokens for setup. After deployment, navigate to /admin/setup to create your admin account. The first user becomes the admin and registration locks automatically.

Troubleshooting

CORS errors

CORS_ORIGIN must exactly match the web app URL — including protocol (https://), no trailing slash. In production it equals https://SITE_DOMAIN.

Authentication not working

  • Verify BETTER_AUTH_SECRET is set
  • Verify BETTER_AUTH_URL matches the URL the browser actually reaches the API on (https://SITE_DOMAIN in production)
  • If you are on workers.dev, the web and API Workers are different origins and the session cookie cannot cross them. Set SITE_DOMAIN and redeploy

Changes not taking effect

  • NEXT_PUBLIC_SERVER_URL and NEXT_PUBLIC_SITE_URL are baked into the frontend at build time. Changing either requires a redeploy (bun ship rebuilds).
  • Server-side variables take effect on the next request after a redeploy.

On this page