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.mdin 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.comwith 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_DOMAINand 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) ownsSITE_DOMAINas a Cloudflare Custom Domain; the API worker (linkden-api) gets RoutesSITE_DOMAIN/api/*andSITE_DOMAIN/trpc/*. Routes run before the custom-domain worker, soBETTER_AUTH_URL,NEXT_PUBLIC_SERVER_URL,CORS_ORIGIN, andNEXT_PUBLIC_SITE_URLare allhttps://SITE_DOMAINin production. UnsetSITE_DOMAINdeploys toworkers.dev(staging). - Service binding
APIfrom the web worker to the API worker. SSR fetches go throughapps/web/src/lib/server-api.ts(apiFetch), because a workerfetch()to its own zone bypasses routes. Falls back toNEXT_PUBLIC_SERVER_URLin local dev. NEXT_PUBLIC_SITE_URL(validated withz.url()inpackages/env/src/web.ts) drivesmetadataBase,robots.ts, andsitemap.ts. Theexample.comfallbacks are gone; thelocalhostdefault only applies outside production builds, andalchemy.run.tsfails the deploy when the variable is empty or does not matchhttps://SITE_DOMAIN.- Explicit Alchemy stage (
ALCHEMY_STAGE, defaultprod) and resource names:linkden-db,linkden-images,linkden,linkden-api,linkden-backups(R2, 30-day lifecycle expiry). Non-prodstages get the stage as a name suffix so adevdeploy is independent of production. Upgrade note: installs deployed before this release have Alchemy-generated names (linkden-database-runner,linkden-images-runnerfrom CI); setLINKDEN_DB_NAMEandLINKDEN_IMAGES_BUCKETto those before deploying or theprodstage creates an empty database and bucket. See the Cloudflare self-hosting guide. .github/workflows/backup-db.yml: dailywrangler d1 exporttolinkden-backups/d1/<date>.sql.- Structured JSON error logs: Hono
app.onError, tRPConError, and atry/catcharoundscheduled()inapps/server/src/index.ts. Worker invocation logs enabled viaobservability. /api/healthreportsemail: configured | missingnext todatabaseandversion.- 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, noDomain=). - Shared
Sheetprimitive (Base UI Dialog) with abreakpointprop that auto-closes past its breakpoint; replaces three hand-rolled bottom sheets (#51). ColorFieldcontrastAgainstprop: live WCAG ratio badge and a "Fix" chip backed bygetReadableTextColor; 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/testingin-memory libsql helper (createTestDb) for integration tests;settings-registry.tsdrift 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) throughpackages/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.tomlis in place for dated ignores once it is switched to blocking.nextbumped to 16.2.11 andhonoto 4.13.3.
Changed
apps/web/src/app/api/og/route.tsxmoved toapp/og/route.tsxso the OG image route no longer collides with the/api/*Worker route.layout.tsxand the SEO settings section point at/og.- Better Auth
session.cookieCacheenabled (maxAge: 60, options inpackages/auth/src/auth-options.ts, shared with the cookie test).createContextand/api/uploadpassdisableCookieCachefor 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.changePasswordsendsrevokeOtherSessions: true. Stale cross-subdomain cookie comments removed frompackages/auth/src/index.ts. - Deploy workflow: Node version pinned via
.node-version(24),setup-nodeSHA-pinned,SITE_DOMAINandNEXT_PUBLIC_SITE_URLread from environment variables, both added toturbo.jsonpassThroughEnv. Docs deploy builds withNEXT_PUBLIC_BASE_PATH=/linkden. - Alchemy state moved from the deprecated
D1StateStoreto the sharedalchemy-stateCloudflareStateStoreworker;ALCHEMY_STATE_TOKENis now required at deploy time (#54). - Alchemy runs under Node via
tsx(Bun segfaults on the Alchemy program, exit 132). Deploy secrets declared inturbo.jsonpassThroughEnvso Turborepo strict env mode passes them through (#55). - Wallet editor rebuilt around a
DeviceFramepreview hero with an in-flow translucent sheet; state model, tRPC contract, andwallet_*keys unchanged (#52). - Dashboard and appearance headers render via
PageHeader;DesktopTopBarlabel demoted to a non-heading kicker so each page has oneh1(#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.jsononly, compared withcompareSemver(#53). - Next 16
middleware.tsrenamed toproxy.ts; tsdownnoExternalreplaced withdeps.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-typesscripts in every code workspace;@vitest/coverage-v8with a coverage floor enforced invitest.config.ts(#53).
Removed
- Docker, Coolify, and Railway deployment path and docs. Deployment is Cloudflare-only (#53).
/qrentry dropped fromsitemap.ts.
Fixed
- Password reset on the login page called a nonexistent endpoint on the
web origin; it now uses
authClient.requestPasswordResetagainst the API (#53). - Deploy workflow checks out
workflow_run.head_sha, the exact commit that passed CI (#53). settings.getAllmasked three of six secrets; wallet signing certs were returned in plaintext (#53).audit_logwas 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 (skippedin the result,backup.import.skipped_rowsaudit entry) instead of landing as rawjavascript: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'sclearablefields turn it intonullfor the row (packages/validators/src/blocks.ts). vcardConfigSchemais derived fromvcardDataSchema, 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 withObject.hasOwn, so an importedheadingLevel: "__proto__"can no longer crash the server render.
Security
- Single-admin invariant enforced by a
BEFORE INSERTtrigger onuser(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 iscf-connecting-ip(#53). - Uploads:
Content-Lengthrejected before buffering, magic-byte signature must match the extension, replaced R2 objects deleted (#53). - Rate limits on
/api/auth/request-password-resetand/api/auth/reset-password(#53). - Every GitHub Action SHA-pinned with minimal
permissionsblocks (#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/connectionspanel 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
ImageCropDialogwired intoImageUploadFieldwith per-purpose presets. - Admin settings UX: top tabs + sticky save bar.
social_icon_shapesetting (circle|rounded-square).- Hero-card public-page layout, single-column blocks.
/admin/analyticspage + shared analytics widgets.- Split-panel login, sidebar setup wizard, custom login branding.
/admin/connectionscontact-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:coveragescript.
Changed
/admin/accountredesigned as stacked sections.- README +
CLAUDE.mdrefreshed for the current monorepo layout. - Upload validation extracted into
apps/server/src/lib/upload-validation.tsfor unit testing. - Patched + minor dependency upgrades across workspaces.
Fixed
- Narrowed
getLoginShaderPresetreturn type toShaderBannerPreset.
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_enabledsetting. - 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.