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 namedlinkden-db, or whateverLINKDEN_DB_NAMEsays on a pre-0.5.0 install) - Check the
DBbinding inapps/server/wrangler.jsoncandpackages/infra/alchemy.run.ts - Redeploy with
bun ship: Alchemy adopts the existing database by name and re-binds it. Never runbun run destroyto fix a binding
R2 bucket not working
Image uploads fail with "Image storage not configured".
- Ensure the
linkden-imagesbucket exists and is bound asIMAGES_BUCKETinpackages/infra/alchemy.run.ts - Verify with
bunx wrangler r2 bucket list
CORS errors on workers.dev subdomains
CORS_ORIGINmust exactly match the web Worker URL, includinghttps://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 (SQL parameters, which can hold visitor data, are stripped). Every request also logs one line, METHOD path status ms: the path only, never the query string, and the password-reset token is redacted. Stream them with:
bunx wrangler tail linkden-api --format=prettyThe daily retention job (03:00 UTC) logs each failed step as a JSON line with path scheduled and step prune or orphan-sweep, and the run then shows as failed in Workers → linkden-api → Cron Events.
API answers 503 "authentication is misconfigured"
BETTER_AUTH_SECRET is empty, shorter than 32 characters, or a placeholder value. Sign-in, the admin API and uploads stay off until it is fixed; /api/health reports "auth": "misconfigured". Generate one with openssl rand -base64 32, set it, and redeploy.
Restore a D1 backup
Run every command below from the repo root. DB is your database name: LINKDEN_DB_NAME if you set it, otherwise linkden-db.
1. D1 Time Travel (try this first). D1 keeps a point-in-time history of the database for its retention window (30 days on paid plans, 7 on the free plan). Every GitHub deploy also records a bookmark in its job summary.
DB=linkden-db
bunx wrangler d1 time-travel info "$DB"
bunx wrangler d1 time-travel restore "$DB" --timestamp=2026-10-01T12:00:00Z
# or, to undo a deploy: --bookmark=<bookmark from the deploy summary>The restore rewinds the whole database in place. No redeploy is needed.
2. The nightly export. The backup-db workflow exports the database every day to the linkden-backups R2 bucket as d1/<date>.sql (kept 30 days). It only runs once the production environment secrets are set, and it fails (rather than uploading) when the dump has no user row. An export contains CREATE TABLE statements, so restore it into a fresh database and point the deploy at that:
# Download the export you want
bunx wrangler r2 object get linkden-backups/d1/2026-08-21.sql --file=backup.sql --remote
# Import it into a new database
bunx wrangler d1 create linkden-db-restore
bunx wrangler d1 execute linkden-db-restore --remote --file=backup.sql
# Point production at it and redeploy
LINKDEN_DB_NAME=linkden-db-restore bun shipKeep LINKDEN_DB_NAME=linkden-db-restore set (in .env or as a GitHub production variable) for every later deploy, and point the backup-db workflow at it through the same variable.
Wiping instead?
bun reset:factory -- --confirm --remoteempties every data table in one atomic step (the schema and migration history stay). It asks you to type the database name (or pass--confirm-db <name>). Take an export first:bunx wrangler d1 export "$DB" --remote --output=before-reset.sql.
General Issues
CORS errors
Symptom: Browser console shows Access-Control-Allow-Origin errors.
Fix:
CORS_ORIGINmust exactly match the frontend URL (protocol + domain + port, no trailing slash)- Wrong:
http://localhost:3001/orlocalhost:3001 - Right:
http://localhost:3001
Auth cookie issues
Symptom: Login succeeds but you're immediately logged out, or admin pages show "Unauthorized".
Causes:
BETTER_AUTH_URLdoesn't match the actual API URL the browser reaches- Frontend and API are on different origins (the
workers.devcase above) BETTER_AUTH_SECRETchanged between deployments (invalidates existing sessions)
Fix:
- Ensure
BETTER_AUTH_URLmatches the publicly accessible API URL (https://SITE_DOMAINin production) - Set
SITE_DOMAINso 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:
- Go to
/admin/settings/email - Select your provider: Resend (Cloudflare Email Workers coming soon)
- Enter a valid API key
- Ensure the "From" email is verified with your provider
- Check spam folders
Without email, you can still recover the admin account from the command line: bun reset:password -- --email you@example.com --remote.
"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.
Locked out? Reset the password instead: bun reset:password -- --email you@example.com --remote (add --disable-2fa if you also lost your authenticator and backup codes). To start over completely, factory-reset the database (see Restore a D1 backup above).
If the API Worker has a SETUP_TOKEN secret, the first signup also needs that token; a wrong one returns 403 "Invalid setup token".
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.
- 403 error: The upload did not come from your site's own origin (
CORS_ORIGIN). Uploads and admin writes must come from the admin panel itself. - 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, and each value is checked against its rules (length, URL, colour, enum, JSON shape). The error message names the problem. If you're making direct API calls, ensure you're using valid keys and values. The admin UI handles this automatically.
"Too many requests"
The API rate-limits sign-in, two-factor, password reset, magic links, sign-up, uploads, the contact form and analytics per client IP (and caps sign-in attempts globally). Wait a minute and try again. A batched tRPC request counts once per limited procedure in it.