ps-shin
Back to projects
2026·Active

lab

One-screen full-stack demo. Real signup + opaque sessions + per-user task dashboard with filter/search/sort/CRUD. Composes the PS-safe research libraries inside this portfolio's Next.js runtime against Neon.

TypeScriptNext.js 16React 19RSCNeonArgon2id@node-rs/argon2

How it works

lab — single route, two states, URL-driven dashboard

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 an HttpOnly Secure SameSite=Lax cookie, 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. /lab is 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/login page, 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.replace inside useTransition. The RSC re-runs with the new searchParams and 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 against router.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. /login returns the same 401 for "unknown email" and "wrong password" so an attacker can't probe which emails have accounts. /signup returns 409 if 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. PATCH and DELETE on /api/lab/tasks/[id] enforce ownership via WHERE user_id = $1. A foreign caller gets 404, not 403, 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/signup and /login share 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)