From 3c319ed6411a595e6974a86bc58c2a308b91c214 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 14:14:05 +0100 Subject: [PATCH] fix: redirect to setup wizard on fresh deployments when public page is first request On a fresh CF deployment, if the first request hits a public page, the middleware fast-path skips runtime init. Template helpers like getSiteSettings() then query an empty database and crash with 'no such table: options'. Add a one-time setup probe in the middleware fast-path: check if the migrations table exists, and redirect to the setup wizard if not. The check is cached for the worker lifetime after first success. Also includes release workflow update to use GitHub App token and admin branding changeset. --- .changeset/cruel-forks-float.md | 5 ++++ .changeset/quick-parks-smoke.md | 5 ++++ packages/core/src/astro/middleware.ts | 35 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .changeset/cruel-forks-float.md create mode 100644 .changeset/quick-parks-smoke.md diff --git a/.changeset/cruel-forks-float.md b/.changeset/cruel-forks-float.md new file mode 100644 index 0000000..87e0d0f --- /dev/null +++ b/.changeset/cruel-forks-float.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/admin": patch +--- + +Update branding diff --git a/.changeset/quick-parks-smoke.md b/.changeset/quick-parks-smoke.md new file mode 100644 index 0000000..1b31d8d --- /dev/null +++ b/.changeset/quick-parks-smoke.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fix crash on fresh deployments when the first request hits a public page before setup has run. The middleware now detects an empty database and redirects to the setup wizard instead of letting template helpers query missing tables. diff --git a/packages/core/src/astro/middleware.ts b/packages/core/src/astro/middleware.ts index f37dfa7..6747522 100644 --- a/packages/core/src/astro/middleware.ts +++ b/packages/core/src/astro/middleware.ts @@ -54,6 +54,16 @@ let runtimeInitializing = false; /** Whether i18n config has been initialized from the virtual module */ let i18nInitialized = false; +/** + * Whether we've verified the database has been set up. + * On a fresh deployment the first request may hit a public page, bypassing + * runtime init. Without this check, template helpers like getSiteSettings() + * would query an empty database and crash. Once verified (or once the runtime + * has initialized via an admin/API request), this stays true for the worker's + * lifetime. + */ +let setupVerified = false; + /** * Get EmDash configuration from virtual module */ @@ -190,6 +200,28 @@ export const onRequest = defineMiddleware(async (context, next) => { if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) { const sessionUser = await context.session?.get("user"); if (!sessionUser && !playgroundDb) { + // On a fresh deployment the database may be completely empty. + // Public pages call getSiteSettings() / getMenu() via getDb(), which + // bypasses runtime init and would crash with "no such table: options". + // Do a one-time lightweight probe using the same getDb() instance the + // page will use: if the migrations table doesn't exist, no migrations + // have ever run -- redirect to the setup wizard. + if (!setupVerified) { + try { + const { getDb } = await import("../loader.js"); + const db = await getDb(); + await db + .selectFrom("_emdash_migrations" as keyof Database) + .selectAll() + .limit(1) + .execute(); + setupVerified = true; + } catch { + // Table doesn't exist -> fresh database, redirect to setup + return context.redirect("/_emdash/admin/setup"); + } + } + const response = await next(); setBaselineSecurityHeaders(response); return response; @@ -210,6 +242,9 @@ export const onRequest = defineMiddleware(async (context, next) => { // Get or create runtime const runtime = await getRuntime(config); + // Runtime init runs migrations, so the DB is guaranteed set up + setupVerified = true; + // Get manifest (cached after first call) const manifest = await runtime.getManifest();