LinkDen

Changelog

What changed in LinkDen. Each release has a plain-language summary for site owners and a technical section for developers.

TL;DR

  • Latest release: 0.5.0 (2026-08-21)
  • Format: โœจ What's new for site owners + ๐Ÿ”ง Technical for developers and self-hosters.
  • Source of truth: CHANGELOG.md in the repository root.

Each release below has two sections:

  • โœจ What's new โ€” plain-language summary for site owners.
  • ๐Ÿ”ง Technical โ€” internal changes, refactors, security details for developers and self-hosters.

The canonical version number lives in version.json at the repository root. This page mirrors CHANGELOG.md โ€” that file is the source of truth.

Unreleased

No unreleased changes yet.

0.5.0 โ€” 2026-08-21

โœจ What's new

  • LinkDen deploys for real now. The production deploy pipeline works end to end on Cloudflare. Your site and its API live on one hostname (for example https://links.yourdomain.com with the API under /api/*), so there is no cross-site cookie juggling and admin login no longer bounces you back to the login page.
  • Custom domain in one setting. Set SITE_DOMAIN and redeploy. LinkDen attaches the domain to the web app and routes the API under it. No dashboard clicking, no manual DNS records.
  • Daily database backups. A scheduled job exports the D1 database every day into a private R2 bucket (linkden-backups), kept for 30 days. Restore steps are in the troubleshooting docs.
  • You will know when email is not set up. The dashboard shows a banner when no email API key is configured (password reset and magic links need it) with a direct link to the Email settings tab.
  • Wallet pass editor, iOS style. The Apple Wallet builder now looks and works like the Wallet app: a centered pass preview with a bottom tab bar (Business, Images, Context, Colors, Background), live AA contrast badges on custom colours, and an "Unsaved" chip so you never lose an edit.
  • Better keyboard and screen-reader support. Bottom sheets (mobile preview, block editor, connections detail) trap focus, close on Escape, and restore focus when dismissed. Every admin page has one real heading. Colour fields warn you when text will be hard to read and offer a one-tap fix.
  • Theme preset names. "Light" is now "Classic" and "Dark" is "Graphite". Your saved choice is unchanged.
  • Contact form delivery works. Email, database, and "both" delivery modes now do what they say. Submissions are stored before the email goes out, so a mail failure never loses a message.
  • Password reset works. The reset link on the login page now calls the right endpoint.
  • Faster admin sessions. Session lookups for page loads are cached in the cookie for one minute, so admin pages stop hitting the database on every request. Anything that changes data still checks the database, and changing your password now signs out every other browser.

๐Ÿ”ง Technical

Added

  • Same-origin routing model in packages/infra/alchemy.run.ts: the web worker (linkden) owns SITE_DOMAIN as a Cloudflare Custom Domain; the API worker (linkden-api) gets Routes SITE_DOMAIN/api/* and SITE_DOMAIN/trpc/*. Routes run before the custom-domain worker, so BETTER_AUTH_URL, NEXT_PUBLIC_SERVER_URL, CORS_ORIGIN, and NEXT_PUBLIC_SITE_URL are all https://SITE_DOMAIN in production. Unset SITE_DOMAIN deploys to workers.dev (staging).
  • Service binding API from the web worker to the API worker. SSR fetches go through apps/web/src/lib/server-api.ts (apiFetch), because a worker fetch() to its own zone bypasses routes. Falls back to NEXT_PUBLIC_SERVER_URL in local dev.
  • NEXT_PUBLIC_SITE_URL (validated with z.url() in packages/env/src/web.ts) drives metadataBase, robots.ts, and sitemap.ts. The example.com fallbacks are gone; the localhost default only applies outside production builds, and alchemy.run.ts fails the deploy when the variable is empty or does not match https://SITE_DOMAIN.
  • Explicit Alchemy stage (ALCHEMY_STAGE, default prod) and resource names: linkden-db, linkden-images, linkden, linkden-api, linkden-backups (R2, 30-day lifecycle expiry). Non-prod stages get the stage as a name suffix so a dev deploy is independent of production. Upgrade note: installs deployed before this release have Alchemy-generated names (linkden-database-runner, linkden-images-runner from CI); set LINKDEN_DB_NAME and LINKDEN_IMAGES_BUCKET to those before deploying or the prod stage creates an empty database and bucket. See the Cloudflare self-hosting guide.
  • .github/workflows/backup-db.yml: daily wrangler d1 export to linkden-backups/d1/<date>.sql.
  • Structured JSON error logs: Hono app.onError, tRPC onError, and a try/catch around scheduled() in apps/server/src/index.ts. Worker invocation logs enabled via observability.
  • /api/health reports email: configured | missing next to database and version.
  • Dashboard email-missing banner linking to /admin/settings?tab=email (the settings page now reads ?tab).
  • Tests: apps/server/src/__tests__/health.test.ts, apps/web/src/app/__tests__/sitemap.test.ts, packages/auth/src/__tests__/cookie-config.test.ts (Secure, HttpOnly, SameSite=Lax, no Domain=).
  • Shared Sheet primitive (Base UI Dialog) with a breakpoint prop that auto-closes past its breakpoint; replaces three hand-rolled bottom sheets (#51).
  • ColorField contrastAgainst prop: live WCAG ratio badge and a "Fix" chip backed by getReadableTextColor; wired into appearance custom colours and block custom bg/text colours (#51).
  • Tabs variant="bar" (icon over label, roving tabindex) for the iOS style wallet editor (#52).
  • @linkden/db/testing in-memory libsql helper (createTestDb) for integration tests; settings-registry.ts drift test; CAPTCHA, upload-signature, retention, embed-registry, and batch-rollback tests (#53).
  • Daily retention cron (scheduled() handler) pruning analytics, sessions, contacts, and audit rows, plus an orphan R2 image sweep with a 24h grace window (#53).
  • Contact form delivery (email / database / both) through packages/email (#53).
  • CI security job: OSV scanner plus a CycloneDX SBOM artifact (#53). The scan is report-only for now (open advisories are all dev-tooling transitives); osv-scanner.toml is in place for dated ignores once it is switched to blocking. next bumped to 16.2.11 and hono to 4.13.3.

Changed

  • apps/web/src/app/api/og/route.tsx moved to app/og/route.tsx so the OG image route no longer collides with the /api/* Worker route. layout.tsx and the SEO settings section point at /og.
  • Better Auth session.cookieCache enabled (maxAge: 60, options in packages/auth/src/auth-options.ts, shared with the cookie test). createContext and /api/upload pass disableCookieCache for non-GET requests, so mutations always read the session table and a factory reset or revoke takes effect immediately; GET reads may lag by up to 60 s. changePassword sends revokeOtherSessions: true. Stale cross-subdomain cookie comments removed from packages/auth/src/index.ts.
  • Deploy workflow: Node version pinned via .node-version (24), setup-node SHA-pinned, SITE_DOMAIN and NEXT_PUBLIC_SITE_URL read from environment variables, both added to turbo.json passThroughEnv. Docs deploy builds with NEXT_PUBLIC_BASE_PATH=/linkden.
  • Alchemy state moved from the deprecated D1StateStore to the shared alchemy-state CloudflareStateStore worker; ALCHEMY_STATE_TOKEN is now required at deploy time (#54).
  • Alchemy runs under Node via tsx (Bun segfaults on the Alchemy program, exit 132). Deploy secrets declared in turbo.json passThroughEnv so Turborepo strict env mode passes them through (#55).
  • Wallet editor rebuilt around a DeviceFrame preview hero with an in-flow translucent sheet; state model, tRPC contract, and wallet_* keys unchanged (#52).
  • Dashboard and appearance headers render via PageHeader; DesktopTopBar label demoted to a non-heading kicker so each page has one h1 (#51).
  • Theme preset display labels: "Light" โ†’ "Classic", "Dark" โ†’ "Graphite" (persisted keys untouched) (#51).
  • Settings metadata (sanitizer kind, max length, secret/mask, backup policy) consolidated into packages/validators/src/settings-registry.ts; settings, wallet, and backup routers derive from it (#53).
  • Backup import, block reorder, bulk settings, wallet config, and danger resets run as a single db.batch([...]) (#53).
  • Version read from version.json only, compared with compareSemver (#53).
  • Next 16 middleware.ts renamed to proxy.ts; tsdown noExternal replaced with deps.alwaysBundle; dead Wallet "coming soon" branch removed (#53).
  • Shared email, wallet JSON parsing, and embed-provider helpers (parsePass*Json, one embed registry in validators) (#53).
  • check-types scripts in every code workspace; @vitest/coverage-v8 with a coverage floor enforced in vitest.config.ts (#53).

Removed

  • Docker, Coolify, and Railway deployment path and docs. Deployment is Cloudflare-only (#53).
  • /qr entry dropped from sitemap.ts.

Fixed

  • Password reset on the login page called a nonexistent endpoint on the web origin; it now uses authClient.requestPasswordReset against the API (#53).
  • Deploy workflow checks out workflow_run.head_sha, the exact commit that passed CI (#53).
  • settings.getAll masked three of six secrets; wallet signing certs were returned in plaintext (#53).
  • audit_log was skipped by full and factory resets (#53).
  • Wallet "Unsaved changes" chip failed AA on the translucent panel; keyboard focus is kept when switching editor tabs (#52).
  • Dashboard chrome used hardcoded blue/violet (about 2.5:1 on the day theme); consent banner "Accept Selected" and footer pills now read on light presets (#51).
  • Setup wizard surfaces real server errors instead of swallowing them (#51).
  • Backup imports are sanitized; hex-alpha styling guarded (#51). Imported blocks and social networks now go through the same gate as blocks.create / social.updateBulk (validateBlockImport, socialNetworkUpdateSchema): http(s)-only URLs, icon format, per-type config. Invalid rows are skipped and counted (skipped in the result, backup.import.skipped_rows audit entry) instead of landing as raw javascript: hrefs on the public page. The LinkStack importer drops non-http(s) links the same way.
  • Clearing a block's URL, icon, embed URL, or embed type in the builder now persists: the editor sends "" and the schema's clearable fields turn it into null for the row (packages/validators/src/blocks.ts).
  • vcardConfigSchema is derived from vcardDataSchema, so a vCard block the builder accepts always passes the stricter download-path parse (/api/vcard, public.getVCard) instead of silently 404ing.
  • Public renderers guard stored URLs (safeHttpUrl) and look up heading levels / icon names with Object.hasOwn, so an imported headingLevel: "__proto__" can no longer crash the server render.

Security

  • Single-admin invariant enforced by a BEFORE INSERT trigger on user (0007_single_admin_trigger.sql), not just middleware (#53).
  • Analytics and CAPTCHA use server-derived requestMeta (cf-connecting-ip, cf-ipcountry, UA, referer); CAPTCHA provider is an enum that fails closed, with a 10s timeout, remoteip, and hostname/action verification. Better Auth IP header is cf-connecting-ip (#53).
  • Uploads: Content-Length rejected before buffering, magic-byte signature must match the extension, replaced R2 objects deleted (#53).
  • Rate limits on /api/auth/request-password-reset and /api/auth/reset-password (#53).
  • Every GitHub Action SHA-pinned with minimal permissions blocks (#53).
  • Danger resets, wallet config/preset/signing-key changes, and block deletes are written to the audit log (#53).

0.4.0 โ€” 2026-05-10

โœจ What's new

  • Build a proper Apple Wallet pass. New pass builder lays out your card like a real membership pass โ€” header, primary line, secondary fields, image slots, signing keys, and a live preview as you type.
  • Crop every image you upload. Drag a photo in for your avatar, banner, OG image, or wallet logo โ€” a built-in cropper opens with the right shape pre-set so nothing comes out stretched.
  • Cleaner settings page. Settings are now organised into tabs at the top with a sticky save bar at the bottom โ€” no more scrolling to find the save button.
  • Round or rounded-square social icons. Pick which shape suits your page in Appearance.
  • Big-photo "hero card" layout. New public-page layout puts a tall avatar card up top and stacks your blocks beneath it.
  • Analytics page inside the admin. See views and clicks without leaving the dashboard.
  • Custom login screen. The split-panel login is now fully whitelabel โ€” your logo, your colours, your background image.
  • Setup wizard. First-time admins get walked through account, profile, and theme on a single guided screen.
  • Contact-form inbox. All visitor messages land in a new /admin/connections panel with read/unread state.
  • Account page redesign. Profile, email, password, and danger zone are now in calm stacked sections instead of one long form.

๐Ÿ”ง Technical

Added

  • Wallet pass builder (pass.mk-style) with Apple HIG field limits (header / primary / secondary / auxiliary / back), template presets, image slots, signing-keys section.
  • Universal ImageCropDialog wired into ImageUploadField with per-purpose presets.
  • Admin settings UX: top tabs + sticky save bar.
  • social_icon_shape setting (circle | rounded-square).
  • Hero-card public-page layout, single-column blocks.
  • /admin/analytics page + shared analytics widgets.
  • Split-panel login, sidebar setup wizard, custom login branding.
  • /admin/connections contact-form inbox.
  • CI workflow: lint / type-check / test / format / build gate.
  • Unit tests for wallet + settings validators, sanitize utilities, and upload validation โ€” focused on security and data contracts.
  • Vitest v8 coverage config + bun run test:coverage script.

Changed

  • /admin/account redesigned as stacked sections.
  • README + CLAUDE.md refreshed for the current monorepo layout.
  • Upload validation extracted into apps/server/src/lib/upload-validation.ts for unit testing.
  • Patched + minor dependency upgrades across workspaces.

Fixed

  • Narrowed getLoginShaderPreset return type to ShaderBannerPreset.

Security

  • Per-route Cloudflare rate limiting (RL_AUTH, RL_STRICT, RL_UPLOAD, RL_PUBLIC).
  • Triple-check upload validation: size โ‰ค 5 MB, extension allow-list, MIME allow-list, purpose allow-list.
  • Single-user signup lock on /api/auth/sign-up.
  • Magic-link runtime gate via magic_link_enabled setting.
  • Security headers on every response (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy).
  • R2 image-serving path-traversal guard.

0.3.0

โœจ What's new

Baseline release. Public profile page, six block types (link, header, embed, connect, vCard, location), block-level analytics, admin appearance / SEO / branding panels, vCard export, Apple Wallet pass MVP, Apple Maps embed for Location blocks, Cloudflare Workers + D1 hosting.

๐Ÿ”ง Technical

Documentation sync at commit 1691cc5. Stack: Hono on Workers, tRPC v11, Drizzle ORM on D1, Better Auth, Alchemy IaC for deployment.

On this page