How it works
Why this exists
Seven Go libraries on the Projects page evidence backend
craft. That's only half the story I want a senior reviewer to see —
the other half is "can this person ship a polished frontend?" The
live /lab is the single artifact that answers both at once:
sign up, log in, drive a small dashboard, then open the source.
Same idea as the shortlink / ratelimit / webhookd in-portfolio runtime ports — but at full app shape rather than one widget per page.
What it does
- Sign up with email + password → Argon2id-hashed, opaque
session token (
sha256(token)in the DB, raw token in anHttpOnly Secure SameSite=Laxcookie, 7-day TTL) - Land on a per-user dashboard of "tasks" (status: active / pending / archived)
- Filter (multi-select chips), search (debounced text across title + body), sort (created_at / title / status, asc/desc), paginate (8/page)
- Create / edit / delete tasks via a focus-trapped native
<dialog> - Sign out cleanly
All view state — filter, search, sort, page — lives in the URL. The
dashboard is shareable: copy /lab?status=active,pending&order=title
to a teammate and they see the same view after they sign in.
Design decisions worth noting
- One URL, two states.
/labis one route. The Server Component reads the session cookie and decides at render-time whether to show the auth panel or the dashboard. No/lab/loginpage, no redirect waterfall, no client-side "am I signed in" flash. - URL is the source of truth for view state. Filter chips, search
input, sort dropdown, page number — every one writes to the URL via
router.replaceinsideuseTransition. The RSC re-runs with the newsearchParamsand streams a fresh page of tasks. No TanStack Query, no Zustand, no Jotai — the URL is the only store. - Optimistic mutations via React 19's
useOptimistic. Create, edit, and delete update the UI immediately and reconcile againstrouter.refresh()'s next server snapshot. Errors surface inline; on success the optimistic state silently merges with the truth. - Native
<dialog>for both modals. Focus trap, Esc to close, backdrop click — all free from the platform. No Headless UI, no Radix Dialog, no custom focus-management hook. - Argon2 cold-start UX is named in the copy. First signup on a cold Vercel function pays a ~2-4s path (function init + native binding init + 64 MiB Argon2id hash). The submit button reads "Setting up secure password storage…" — the wait is visible and explained, instead of looking like a frozen page.
- Anti-enumeration on
/login, deliberate enumeration on/signup./loginreturns the same401for "unknown email" and "wrong password" so an attacker can't probe which emails have accounts./signupreturns409if the email is taken — that does leak enumeration, and it's an accepted trade-off for the demo (the production answer routes through a mailer-driven verify-or-recover email; mailer is deferred from v1). The trade-off is part of what the source is meant to show. - 404 (not 403) on foreign-owned task IDs.
PATCHandDELETEon/api/lab/tasks/[id]enforce ownership viaWHERE user_id = $1. A foreign caller gets404, not403, so they can't enumerate which IDs exist on other accounts by status code. - Three colors total on the dashboard surface. Accent (active), amber (pending), muted (archived). Color is never the only signal — every status chip pairs a dot and a text label. The portfolio's ambient blue glow + cursor spotlight + 3D card tilt are deliberately turned off here — tables can't be toys.
Why it's inside the portfolio (and not its own service)
Same answer as shortlink /
webhookd: running another deployment for one
demo means another platform account, cross-service auth, two cold-
start budgets, one more thing to monitor. The lab is a one-screen
demo of patterns I'd reach for in a real product; it doesn't earn its
own host. The Go libraries in the Libraries section are
the artifacts I'd carry between employers — /lab is the proof that
I can wire them up.
Source
| Where | What |
|---|---|
| app/lab/ | Page (RSC + auth gate), planning doc, schema migration |
| app/api/lab/ | 8 routes: signup / login / logout / me + tasks CRUD |
| lib/lab/ | argon2 + session helpers; Neon queries; TS port of the queryhelper Spec→Page contract |
| components/lab/ | 10 net-new components — Dashboard, FilterBar, TaskList, TaskRow, TaskFormDialog, DeleteConfirm, Pagination, Skeleton, StatusChip, AuthPanel |
| PLAN.md | The 10-section locked plan this was built from — audience, scope, references, IA, visual language, data model, components, agent team, config, risk register |
Composed libraries
- auth — Argon2id + opaque session pattern (TS port here, Go canonical)
- ratelimit —
/signupand/loginshare a per-IP 5/min bucket - queryhelper — Spec→Page contract for the dashboard's filter/search/sort/page
Future work
- [ ] Email verification on signup (composes mailer once deployed)
- [ ] Password reset
- [ ] Stale-account cleanup cron (Neon free tier has limits)
- [ ] Toast-with-undo on delete (the small dialog works; toast would be tighter)
- [ ] Drag-and-drop reorder inside a status column
- [ ] OAuth providers (Google, GitHub)