LinkDen
Self-Hosting

Troubleshooting

Common issues and solutions for self-hosted LinkDen instances.

Troubleshooting

Solutions to common problems for self-hosted LinkDen instances. For deployment steps, see the Cloudflare guide.


Cloudflare Workers

Worker names are linkden (web) and linkden-api (API). Their configuration lives in apps/server/wrangler.jsonc (local dev bindings) and packages/infra/alchemy.run.ts (what actually gets deployed). There is no wrangler.toml.

D1 binding errors

The API Worker can't find the database.

  • Verify the D1 database exists: bunx wrangler d1 list (it is named linkden-db)
  • Check the DB binding in apps/server/wrangler.jsonc and packages/infra/alchemy.run.ts
  • Redeploy with bun ship to recreate bindings

R2 bucket not working

Image uploads fail with "Image storage not configured".

  • Ensure the linkden-images bucket exists and is bound as IMAGES_BUCKET in packages/infra/alchemy.run.ts
  • Verify with bunx wrangler r2 bucket list

CORS errors on workers.dev subdomains

  • CORS_ORIGIN must exactly match the web Worker URL, including https:// and no trailing slash
  • Example: CORS_ORIGIN=https://linkden.your-subdomain.workers.dev

Admin login loops back to the login page on workers.dev

On workers.dev the web Worker (linkden.<sub>.workers.dev) and the API Worker (linkden-api.<sub>.workers.dev) are separate origins. workers.dev is on the Public Suffix List, so the session cookie set by the API is never sent back from the web origin and /admin bounces to /admin/login on every request.

This is expected. workers.dev is a staging target: the public page and /api/health prove the deploy, nothing more.

Fix: set SITE_DOMAIN to your hostname, point BETTER_AUTH_URL, NEXT_PUBLIC_SERVER_URL, CORS_ORIGIN, and NEXT_PUBLIC_SITE_URL at https://SITE_DOMAIN, and redeploy. The web app gets the hostname as a Custom Domain and the API is routed under it at /api/* and /trpc/*, so everything is one origin. See Custom Domain.

Reading API errors

Unhandled errors in the API are logged as one JSON line with the request method, path, and error message. Stream them with:

bunx wrangler tail linkden-api --format=pretty

Restore a D1 backup

The backup-db workflow exports the database every day to the linkden-backups R2 bucket as d1/<date>.sql (kept 30 days). To restore one:

# 1. Download the export you want
bunx wrangler r2 object get linkden-backups/d1/2026-08-21.sql --file=backup.sql --remote

# 2. Replay it into the live database
bunx wrangler d1 execute linkden-db --remote --file=backup.sql

The export contains CREATE TABLE and INSERT statements. Restoring over a database that already has data will fail on duplicate rows, so either restore into a fresh database or run the reset script first (bun reset:factory, then restore). Take a manual export before you start (bunx wrangler d1 export linkden-db --remote --output=before-restore.sql) so you can undo it.


General Issues

CORS errors

Symptom: Browser console shows Access-Control-Allow-Origin errors.

Fix:

  1. CORS_ORIGIN must exactly match the frontend URL (protocol + domain + port, no trailing slash)
  2. Wrong: http://localhost:3001/ or localhost:3001
  3. Right: http://localhost:3001

Symptom: Login succeeds but you're immediately logged out, or admin pages show "Unauthorized".

Causes:

  • BETTER_AUTH_URL doesn't match the actual API URL the browser reaches
  • Frontend and API are on different origins (the workers.dev case above)
  • BETTER_AUTH_SECRET changed between deployments (invalidates existing sessions)

Fix:

  • Ensure BETTER_AUTH_URL matches the publicly accessible API URL (https://SITE_DOMAIN in production)
  • Set SITE_DOMAIN so web and API share one origin
  • If you changed BETTER_AUTH_SECRET, users need to log in again

Email sending failures

Symptom: Password reset or magic link emails don't arrive.

Causes:

  • Email provider not configured in admin settings
  • API key is invalid or expired
  • From email address not verified with the provider

Fix:

  1. Go to /admin/settings > Email
  2. Select your provider (Resend or Cloudflare Email Workers)
  3. Enter a valid API key
  4. Ensure the "From" email is verified with your provider
  5. Check spam folders

"Registration is closed" message

Symptom: Trying to sign up returns 403 "Registration is closed".

Explanation: This is expected behavior. LinkDen is a single-user platform. After the first admin account is created, registration is permanently locked. This protects your instance from unauthorized account creation.

If you need to reset and create a new admin account, clear the user table in your D1 database using the D1 console in the Cloudflare dashboard (or bunx wrangler d1 execute linkden-db --remote --command "DELETE FROM user;").

File upload errors

Symptom: Image uploads are rejected.

Possible causes:

  • 413 error: File exceeds the 5 MB size limit. Resize or compress the image.
  • 400 error (file type): Only image files are allowed: jpg, jpeg, png, gif, webp, ico.
  • 400 error (signature): The file's bytes don't match its extension (for example an HTML file renamed to .png). Re-export the file in a supported format.
  • 500 error: R2 bucket (Cloudflare) or image storage is not configured. Check your deployment's storage bindings.

Settings validation errors

Symptom: Saving settings fails with a validation error.

Explanation: The settings API only accepts known setting keys. If you're making direct API calls, ensure you're using valid keys. The admin UI handles this automatically.

On this page