Files
hackerspace.sh/content/docs/reference/forms.md
T
Moheeb Zara 16229ae523 feat(docs): add a Diátaxis documentation section to the public site
A new public /docs area covering the whole platform, organized by Diátaxis:
tutorials (learning), how-to guides (tasks), reference (facts), and explanation
(the why). 29 pages, each grounded in the actual routes, actions, schema, and
UI — verified against the code and corrected for accuracy (e.g. admin-only vs
admin+board gating, where the permissions matrix actually lives, how Stripe
reconciliation matches).

Implementation:
- lib/docs/nav.ts is the single source of truth (categories + pages), driving
  the sidebar, static params, and prev/next.
- Content authored as content/docs/<category>/<slug>.md, read at build time
  (the routes are statically generated), rendered by <DocsMarkdown> — reuses the
  existing react-markdown/GFM stack, no new dependencies.
- (docs) route group with its own theme (docs.css) matching the public site's
  dark / monospace / lime aesthetic; sidebar with active-state, breadcrumb,
  prev/next, and a Diátaxis home. Public — signed-in users can read it too.
- "Docs" link added to the landing nav.

typecheck 0 · lint 0 errors · build ok (all /docs pages prerendered)
2026-08-05 00:42:42 -07:00

6.3 KiB
Raw Blame History

Forms and waivers are per-space form definitions with a jsonb field schema, a lifecycle status, and a visibility setting. Every submission snapshots the schema and legal text it was signed against, so a waiver stays valid against exactly what the signer saw even after the form is edited. This page documents the field types, visibility rules, gating, and the immutable submission record. To build one, see Build a form.

Where forms live

Managers work with forms at /forms: created at /forms/new, edited at /forms/{id}/edit, and their submissions viewed at /forms/{id}/results. Public and signed-in forms are filled at /f/{space}/{slug}; members-only forms are filled from inside the app. Managing forms requires the forms.manage permission, seeded to the board role by default (admins always have it via implicit-all). Ordinary members can read only published forms.

Form record

Each form is one row in the forms table.

Field Type Notes
slug text Unique per space. Lowercase letters, numbers, and internal hyphens; 1–80 chars. Immutable after creation.
title text 1–200 chars.
description text Optional, up to 2000 chars.
kind enum form or waiver.
visibility enum public_anon, public_auth, or members.
status enum draft, published, or closed.
schema jsonb Ordered array of field definitions (see below).
legal_text text Optional legal/consent text, up to 100000 chars.
version integer Starts at 1; bumps on waiver edits (see Versioning).

The slug is unique per space (UNIQUE (space_id, slug)), not globally, so two spaces can use the same slug.

Field types

The schema is an ordered array of fields. Each field has a key (lowercase letters, numbers, underscores; 1–60 chars; unique within the form), a label, an optional help string, and an optional required flag (default false). A form may have up to 200 fields.

type Answer stored as Server validation
short_text trimmed string max 2000 chars
long_text trimmed string max 20000 chars
email trimmed, lowercased string must match an email pattern
number number must be finite
date string must parse as a date
checkbox boolean if required, must be checked (true)
select string must be one of options
radio string must be one of options

select and radio fields require an options array (each option 1–200 chars, up to 100 options). Answers are validated server-side against the stored schema on submit: unknown keys are discarded, empty required fields are rejected, and only known fields are persisted.

Member linking

A submission links to a member when its submitter_email matches a member in the same space (case-insensitive); if several members share an email, the earliest-joined one is chosen. For members-only and signed-in forms the linked member and email come from the authenticated session. For anonymous public submissions the email may be typed or derived from an email-type answer; linking by a typed email is an intentional attribution-only behavior that grants no access. Managers can re-run linking across the space, and members can retro-claim their own prior anonymous submissions once their email is verified.

Visibility

Visibility controls who can open and submit a form.

visibility UI label Who can submit Public URL
public_anon Public Anyone, no sign-in /f/{space}/{slug}
public_auth Signed in Any signed-in user /f/{space}/{slug}
members Members only Members of the space Not served publicly; filled in-app

Members-only forms are never returned by the public page, so their schema is not exposed to non-members. The public read path serves only a published form and never a members form. A form must be published to accept responses regardless of visibility; draft and closed forms reject submissions.

Required-form gating

A class definition can reference a form as its required_form_id. When set, the form gates signups for that class's sessions: a member cannot sign up until they have a submission on file for that form. The referenced form must be published before it can gate signups. See Classes and sessions and the class screens under /classes.

Submission snapshots

Submissions are rows in form_submissions. Each row captures a point-in-time copy of the form so the record is self-contained:

Field Notes
answers jsonb, keyed by field key; only known, validated fields.
form_snapshot The field schema as it was at submit time.
legal_text_snapshot The legal text as it was at submit time.
form_version The form's version at submit time.
member_id Linked member, if resolved (see below).
submitter_email Provided or derived email, lowercased.
ip, user_agent, created_at Request metadata and timestamp.

Submissions are immutable and append-only. The form_submissions table has no client insert, update, or delete policy under row-level security, so every submission is written by one validated server action using the service client, after server-side schema validation and snapshotting. Managers can read submissions (forms.manage) and permanently delete a single submission from the results screen. Deleting a form cascades and permanently removes all of its submissions, including signed waivers; this requires an explicit confirmation.

Results are viewable at /forms/{id}/results and can be exported to CSV. The CSV header is submitted_at, form_version, member_id, submitter_email, ip, followed by one column per field key.

Waivers and versioning

A form with kind set to waiver requires the submitter to agree (a consent flag must be true) before the submission is accepted. A form's version starts at 1. Editing bumps the version only when it is a published waiver and either the legal text or the field schema actually changed. This is a non-blocking re-sign: existing submissions stay valid against their own snapshot, and only new signers see the bumped version. Ordinary forms and draft waivers never bump their version on edit.