Build Gate Screen — code entry and URL parameter flow #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Task 5 — Plan reference:
docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.mdUser story (US-AUTH-001 + US-AUTH-002):
?code=AB3K7MN2and am immediately logged in and redirected to the gallery.Acceptance criteria
GET /?code=AB3K7MN2validates code server-side, setsfamily_codeHTTP-only cookie, redirects to/galerie(US-AUTH-001)GET /with validfamily_codecookie already set → immediately redirects to/galerie(no gate shown)/galerie(US-AUTH-002)font-mono,uppercase,tracking-[4px], centered,maxlength="8",inputmode="text",autocomplete="off"🏡icon, hint text at bottomDesign spec reference
View 01 in
docs/superpowers/specs/2026-05-05-erbstuecke-wannsee-views.htmlFiles to create
src/routes/+page.sveltesrc/routes/+page.server.tsDepends on: #3 | Size: XS | Spec: reservierung-design §4.1, views spec View 01
👤 Markus Keller — Application Architect
Observations
+page.server.ts. This is the right shape: one route, one file, two entry points (GET load + POST action).GET /?code=AB3K7MN2validates server-side, sets cookie, and redirects. This must live in theloadfunction, not in a Form Action — correct, because it is triggered by a GET.family_codecookie already set → immediately redirects to/galerie". This redirect belongs inhooks.server.tsor at the top of theloadfunction, not scattered into middleware. Placing it inloadis fine for a single-route gate; the hook approach is cleaner if other routes later need the same check.use:enhancelayer is additive, not load-bearing.locals.familyCodemust be populated byhooks.server.tsbefore the load function runs. The issue depends on#3(which presumably sets up thehooks.server.tsauth layer and thedb.tssingleton). This dependency is correctly called out./api/prefix, noonMountfetch — the route tree matches the architecture spec exactly.+page.server.tsfor this screen needs: (1) a prepared statement against thecodestable, (2) the cookie-set helper fromlib/auth.ts, and (3) aredirect(303, '/galerie')on success. All three belong inlib/, not inlined in the route.Recommendations
loadfunction to checklocals.familyCodefirst (already set by hooks) and redirect immediately, before touchingurl.searchParams. This keeps the fast path at the top.?code=parameter validation inload, do not create a new DB query inside the load body — use the prepared statement exported fromlib/db.ts(stmtCodeByValue). It must be prepared at module load time, not per-request.fail(401, { error: 'code_not_found' })on invalid code — never expose the raw code value in the error body. The template maps the error key to the German string.redirect(303, '/galerie')after successful cookie-set in both paths (URL param and form). SvelteKit'sredirect()throws, so no explicitreturnis needed afterward.SCHEMA_SQLfromlib/db.tsso integration tests can set up an identical:memory:database for this route's action.Open Decisions (omit if none)
hooks.server.ts(applies globally to/) or at the top of+page.svelte'sload. Inhooks.server.tsit is DRY if other unauthenticated routes are added later; inloadit is more explicit and co-located with the gate logic. For a single-route gate, either works — but the choice should be made once and documented so it isn't revisited per-route.👤 Felix Brandt — Fullstack Developer
Observations
src/routes/+page.svelteandsrc/routes/+page.server.ts. The route shape is correct — gate screen lives at the root route.+page.server.tsneeds two entry points: theloadfunction (handles?code=URL param) and adefaultForm Action (handles manual POST). The acceptance criteria map cleanly to these two.+page.svelteis a pure form component — no$stateneeded beyond maybe asubmittingflag for theuse:enhanceloading state. No$derivedlogic is required here; all branching is server-side.form?.errorreturned byfail(401, { error: 'code_not_found' }). The template renders the German string, not the raw key.font-mono,uppercase,tracking-[4px], centered,maxlength="8",inputmode="text",autocomplete="off". TheuppercaseCSS class on the input does not uppercase the submitted value server-side — the server must call.toUpperCase()on the form data value before DB lookup.onMount+ fetch. No client-side validation. All validation in the Form Action. The component receivesdata(fromload) andform(from action result).use:enhancedirective makes the form work with JS but must not be required for the no-JS path (AC: "Works without JavaScript").Recommendations
const code = String(formData.get('code') ?? '').trim().toUpperCase(). Validate:if (!code || code.length !== 8) return fail(400, { error: 'code_invalid_format' }). Then look up the DB. This is the correct order: format check → DB lookup.redirect(303, '/galerie')(imported from@sveltejs/kit) on success in bothloadand the action. In the action, it throws — do not return after calling it.+page.svelte, binduse:enhanceto setsubmitting = $state(false)so the button shows a loading state when JS is available. The button text can switch to "Bitte warten…" while submitting — but the form must submit and work identically without JS.<input>element needsname="code"to matchformData.get('code')on the server. This is the most common omission on gate screens.form?.errorvalues to user strings in the template — never renderform?.errordirectly:loadfunction readslocals.familyCode(set byhooks.server.ts) and redirects to/galerieif already set. It then readsurl.searchParams.get('code'), looks it up, sets the cookie, and redirects. If the param is absent or invalid, it returns{}(empty — renders the gate form).Open Decisions (omit if none)
uppercaseCSS class. This visually uppercases but does not uppercase the submitted value in all browsers consistently. The server must normalize regardless. The question is whether to also add a JSoninputhandler that uppercases in real time for UX — this is a progressive enhancement decision (no impact on correctness, small UX improvement). Mark as a UI polish call.👤 Nora "NullX" Steiner — Application Security Engineer
Observations
httpOnly: true,sameSite: 'strict',secure: !dev,path: '/', and a meaningfulmaxAge. The issue does not specifymaxAge— the system design implies 30 days for family members. This must be explicit inlib/auth.ts.ABCDEFGHJKLMNPQRSTUVWXYZ23456789— 32 chars) = 32^8 ≈ 10^12 combinations. Not brute-forceable at normal web request rates. No rate limiting is required for MVP at this scale (family-only app, short-lived). However, SvelteKit has no built-in rate limiting — Caddy can add it at the proxy layer if needed.input_codeback in the error (CWE-209).url.searchParams.get('code')in theloadfunction. The value must be passed through.trim().toUpperCase()and length-checked before the DB query. A 10,000-character string in the URL parameter should not reach the prepared statement.loadvs. action: Setting the cookie from aloadfunction is supported in SvelteKit viaevent.cookies.set(). Theloadfunction receives the fullRequestEvent. This is correct for the GET?code=flow.Recommendations
lib/auth.ts, define the cookie-set helper with all required options hardcoded — never leavesameSite,httpOnly, orsecureto caller discretion:const param = (url.searchParams.get('code') ?? '').trim().toUpperCase().slice(0, 8). The.slice(0, 8)bounds the input before it touches any query.sanitizeCode(raw: string): stringhelper inlib/auth.tsto avoid drift between the two paths.lib/db.tsis parameterized:db.prepare('SELECT * FROM codes WHERE code = ?')— never string-interpolated. This is CWE-89 (SQL injection) prevention.Open Decisions (omit if none)
rate_limitvia a plugin, but this adds operational complexity. The decision is: accept the theoretical risk for MVP (probability near zero, no PII at stake beyond family item preferences), or add Caddy-level rate limiting now. This is a risk-tolerance call for the project owner, not a technical constraint.👤 Sara Holt — QA Engineer & Test Strategist
Observations
src/lib/db.test.ts,auth.test.tsare listed in the plan but not created). This is Issue #5 — the gate screen — and the plan marks it as depending on Issue #3 (project scaffold + hooks). The test infrastructure must exist before tests for this issue can run.load, manual entry in action) are distinct server behaviors that each need an integration test. They are not the same code path — do not cover them with a single test.fetch(no JS) or by disabling JS in Playwright and navigating to/?code=VALID.Recommendations
Unit tests (Vitest,
lib/auth.ts):Integration tests (Vitest +
:memory:SQLite,src/routes/+page.server.ts):GET /?code=VALID8CHR→ setsfamily_codecookie, redirects 303 to/galerieGET /?code=BADCODE1(not in DB) → renders gate form (no redirect, no cookie set)POST /with valid code → sets cookie, redirects 303 to/galeriePOST /with unknown code →fail(401, { error: 'code_not_found' })POST /with malformed code (length ≠ 8) →fail(400, { error: 'code_invalid_format' })GET /with validfamily_codecookie already set → redirects 303 to/galerie(no gate rendered)E2E (Playwright):
Accessibility (axe-core in Playwright):
:memory:SQLite withSCHEMA_SQLfromlib/db.ts. Never share a DB instance across tests.RequestEvent. Playwright E2E is for the happy path and the no-JS path.Open Decisions (omit if none)
beforeEach, or (b) expose a test-onlycreateCodehelper fromlib/auth.ts. Option (a) is simpler and keeps test data co-located with the test. Option (b) is more reusable across multiple test files. For a project of this size, option (a) is sufficient — but decide once and apply consistently across all route tests. (Raised by: Sara Holt)👤 Leonie Voss — UI/UX Design Lead & Accessibility Strategist
Observations
aria-hidden="true"). The card title "Erbstücke Wannsee" is the page's<h1>(Lora, 20px/700). The page<title>must also be "Erbstücke Wannsee" per AC.<label>element. The spec shows no visible label (the subtext serves as the visual prompt). Asr-onlylabel is mandatory for screen reader users — the input cannot be identified as "edit text" alone.⚠icon + text — color alone is not sufficient (WCAG 1.4.1). The spec table confirms: "⚠ Icon + Text — kein Farbe allein". The error must also be announced by screen readers — userole="alert"oraria-live="assertive"on the error container.text-[10px]— the spec permits this as a below-minimum exception for hint text only. It is not body copy. Do not apply this font size anywhere else.min-h-[48px]full-width — correct touch target (exceeds the 44px minimum). The spec showsmin-height: 48pxspecifically for this button.h-12(48px), withtracking-[4px],font-mono,uppercase,text-center. Thespellcheck="false"attribute must be added to prevent browser spell-check from underlining the code characters.Recommendations
sr-onlylabel for the code input:aria-liveregion so screen readers announce the error after form submission without a page reload (when JS is active):<title>via SvelteKit's<svelte:head>:bg-[#DFF0E6]— this is not a design token. It should either become a token (--color-free-bgor similar) or be accepted as a one-off. If the same shade appears elsewhere (the "Meine Reservierung" badge uses#E8F5EC), standardize on one value.→may be read as "right-pointing arrow" depending on the screen reader. Either usearia-label="Weiter"on the button and keep the arrow asaria-hidden, or use the word "Weiter" without the symbol.focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primaryon the submit button as well, to ensure keyboard users can see focus state. Never rely on browser defaults alone.Open Decisions (omit if none)
#DFF0E6icon background color: This value appears in the spec as a one-off for the gate icon. It is close to but not identical to the "Meine Reservierung" badge color (#E8F5EC). Should this be consolidated into a single token, or documented as two intentionally distinct greens? This affects the design system's token count and must be decided before the design system tokens are finalized. (Raised by: Leonie Voss)👤 Tobias Wendt — DevOps & Platform Engineer
Observations
src/routes/+page.svelteandsrc/routes/+page.server.ts— no infrastructure files are in scope. Nothing to review in Dockerfile, docker-compose.yml, or Caddyfile for this issue specifically.loadfunction sets an HTTP-only cookie. Whethersecure: !devevaluates correctly depends on thedevimport from$app/environment— this works correctly in the SvelteKit Node adapter production build and invite dev. No special container config is needed.secure: falsein dev (local HTTP) andsecure: truein production (HTTPS via Caddy). The!devflag handles this correctly with no additional environment variable./healthendpoint referenced in thedocker-compose.ymlhealthcheck (wget -qO- http://localhost:3000/health || exit 1) is not part of this issue — it must exist before the container healthcheck can pass. Confirm it is created in an earlier issue (likely Task 1 or Task 3). If not, flag it.SESSION_SECRETenvironment variable check (if (!process.env.SESSION_SECRET) throw new Error(...)) is inlib/db.tsorhooks.server.ts. This must fire at startup, not at cookie-set time. If it is missing, the container will start, pass the healthcheck, and silently fail on the first login attempt. That is a hidden failure mode.Recommendations
/healthroute (src/routes/health/+server.tsreturning200 OK) is implemented before this issue is marked done. The container healthcheck depends on it. If it is not in an earlier task, add it here — it is a 5-line file.SESSION_SECRETis in place before this issue ships:.envon the server silently degrades security.DATABASE_PATHenvironment variable must also be checked at startup —new Database(undefined)will throw a cryptic error rather than a clear diagnostic. Add:if (!process.env.DATABASE_PATH) throw new Error('DATABASE_PATH is required')..env.exampledoes not need to be updated for this task.curl https://erbstuecke.raddatz.cloud/ returns 200. No additional infra-level verification needed for this specific issue.👤 Elicit — Requirements Engineer
Observations
?code=URL parameter flow) and US-AUTH-002 (manual entry form) are correctly separated as user stories. They share the same route but represent distinct user journeys: the family member who clicks a link vs. the one who types a code manually.?code=is present but the value is syntactically malformed (e.g.,?code=with no value, or?code=Xwith 1 character)? The AC covers "wrong code → error message" but does not explicitly specify whether a malformed URL param silently falls through to the gate form or shows an error. The spec notes section says "Manuell falscher Code → Fehlermeldung" — this implies the URL param path may silently drop to the form without an error. This should be made explicit.?code=param is valid but the gate form is already shown: If a user lands on/?code=VALIDwith an expired or mismatched cookie, the load function clears the old cookie and sets the new one. This path is not called out in the AC but should work by construction.<title>element are the same thing in SvelteKit via<svelte:head>.Recommendations
feature,P1-high,area:auth(or equivalent from the project's taxonomy). Even for a small issue, consistent labeling keeps the backlog filterable.#3should be noted with the specific deliverables it must provide:hooks.server.ts(populateslocals.familyCode),lib/db.ts(exportsstmtCodeByValue), andlib/auth.ts(exportssetFamilyCodeCookie). If#3does not deliver these,#5is blocked.🗳️ Decision Queue — Action Required
5 decisions need your input before implementation starts.
Architecture
hooks.server.ts(globally enforced, DRY if more unauthenticated routes appear later) or at the top of+page.server.ts'sload(explicit, co-located with gate logic). Both are correct for a single-route gate. Cost of hooks: slightly less obvious where the redirect happens. Cost of load: must be manually repeated if a second public route is added. Decide once and document. (Raised by: Markus Keller)Security
rate_limitin the Caddyfile now — adds plugin dependency and operational complexity. For a short-lived family app with ~20 users, option (a) is defensible. (Raised by: Nora Steiner)Testing
codesrow in the:memory:DB. Options: (a) Insert directly inbeforeEach— simple, self-contained. (b) Expose acreateCode()helper fromlib/auth.ts— reusable across all route tests but adds a test-only export. Option (a) is recommended for this project's size. Decide once and apply consistently to all test files. (Raised by: Sara Holt)UI / Accessibility
→symbol may be announced by screen readers as "right-pointing arrow." Options: (a) Keep "Weiter →" as-is and addaria-label="Weiter"to suppress the arrow for AT. (b) Remove the arrow entirely, use "Weiter" only. (c) Wrap the arrow inaria-hidden="true". Option (c) is the minimal change with full accessibility compliance. (Raised by: Leonie Voss)Design System
#DFF0E6gate icon background vs.#E8F5ECbadge background — These are two similar but distinct greens used in different contexts (gate icon halo vs. "Meine Reservierung" badge). Options: (a) Keep both as one-off hardcoded values — simpler, no token bloat. (b) Consolidate into a singlefree-bgtoken — consistent, one value to update. (c) Define two named tokens (free-bg-iconandfree-bg-badge) — semantically precise but adds two tokens for one visual area. This must be resolved before the design systemtailwind.config.jsis finalized. (Raised by: Leonie Voss)