- shared components/forms/copy-link-button.tsx (one control, no drift):
copies the absolute /f/<space>/<slug> URL, check-mark + toast feedback
- admin /forms list: Copy link button on every published public form
- member /my-forms list: shows the /f/<space>/<slug> path and a Copy
button for public forms; row restructured so the button is not nested
in the row link
- members-only forms show no public link (correct: they have none)
337 tests; build verified. Local commit (deploy on your word).
- builder: draft/published/closed status banners; creating a form now
lands on its edit page so Publish is immediately visible; a
members-only form shows a "no public link" note instead of a dead URL
- member-facing forms: /my-forms list + /my-forms/[id] fill
(MemberFillClient), "Forms" sidebar item for all members. Fixes the
long-standing gap where members-only forms had no fill surface
- starter templates: lib/form-templates.ts + a picker on /forms/new
(Liability waiver, Membership application, Event RSVP, Photo release,
Blank) that seeds builder state (no table)
- forms-client relabels members-only forms (was showing a misleading
/f/<space>/<slug> that always 404s for members-only)
- docs corrected per the audit pass: DATABASE_SCHEMA / DB_SCHEMA_MAP /
ARCHITECTURE / API_REFERENCE now reflect migrations 014-029, per-space
slug, /f/[space]/[slug], getPublicForm({space,slug}), invite roles;
HANDOFF pass-19 added with residual gaps
337 tests; build verified (Compiled successfully, exit 0). No deploy.
- migration 028: drop global UNIQUE(forms.slug), add
UNIQUE(space_id, slug); schema.sql mirrored. Form slugs are now
unique per space (fixes cross-tenant slug squatting)
- getPublicForm now takes { space, slug }: resolves the space by its
slug then the form by space_id + slug (maybeSingle)
- route moved app/f/[slug] -> app/f/[space]/[slug]; the public client
submits by formId (slug is no longer globally unambiguous) and the
sign-in link round-trips the full /f/space/slug path
- forms-guard returns the space slug; the builder + forms list show
the full /f/<space>/<slug> share URL; new/edit pass it through
- middleware /f allowlist already covers /f/<space>/<slug>
- getPublicFormSchema + tests updated (333 pass)
build verified (Compiled successfully, exit 0; new route present). No deploy.
The forms security/correctness logic was buried in DB-coupled server
actions and the builder component, so it had no real tests (the suite
is pure-logic only; nothing exercises Supabase). Extract the pure
parts behaviour-unchanged into lib/forms-logic.ts and test them for
real:
- csvCell (results-export escaping), parseClientIp (e-sign IP capture
filtering), shouldBumpFormVersion (waiver non-blocking re-sign),
evaluateRequiredFormStep (onboarding required/auto-satisfy/fail-open),
slugify + deriveFieldKeys (builder key derivation + dedupe)
- forms.ts / onboarding.ts / form-builder.tsx now consume these; the
local copies are removed (no behaviour change — verified by the full
suite staying green)
- __tests__/forms.test.ts expanded 16 -> 62: every validateAnswers
type/branch (type coercion, length caps, required, unknown-key
stripping, first-error ordering), parseFormSchema, every new Zod
schema incl. formSlug boundaries, and all extracted helpers
333/333 tests; build verified (Compiled successfully, exit 0). No deploy.
- components/forms/form-renderer.tsx: one controlled renderer for all 8
field types, shared by the builder preview and the public page
- components/forms/form-builder.tsx: easy builder — label-only field
creation (keys auto-derived and de-duped at save), one-click type
buttons, up/down reordering, inline options editor, live preview;
edit mode adds publish/close/draft/delete and copy-public-link
- /forms directory, /forms/new, /forms/[id]/edit, /forms/[id]/results
(CSV download). Server-gated by lib/forms-guard.ts (forms.manage via
the same RPC the RLS uses; RLS remains the real boundary)
- public app/f/[slug] (outside the app group): getPublicForm serves
only published public forms — members-only forms are never exposed
there; waiver consent, anon email capture, signed-in gate for
public_auth, thank-you state
- sidebar "Forms & waivers" link; sonner toasts (codebase convention)
- ARCHITECTURE + HANDOFF updated
285/285 tests, build clean, all four routes compiled. No deploy.