Contacts
The per-project contact book — who is captured automatically, from where, and what never happens on its own.
Every project has a contact book: Dashboard → Contacts. It lists everyone
who reached the project with an email address, one row per
(project, email) pair, newest first. Rows are captured server-side and
automatically (apps/web/lib/contacts.ts) — the dashboard view only reads
them, so there is no "add contact" form and no import
(apps/web/app/dashboard/contacts/page.tsx).
The two capture paths
Capture never blocks the thing that triggered it
Both paths treat the contact write as a best-effort side effect. A failed contact insert never fails the form submission or the card — the message or card is stored either way, and the miss is only logged.
Form submissions
A submission to a form endpoint captures the
submitter when it carries an email address (apps/web/lib/form-inbound.ts):
- On a form with a builder schema, an email-typed field names the address authoritatively.
- On a legacy schema-less endpoint, the
email/from/senderkey heuristic that already routes the submission decides.
The submission's name field, when present, fills the contact's name.
Public kanban cards
A card created on the public board by a signed-in visitor captures that
visitor's session address; their name comes from auth metadata, never from the
email's local part (createPublicCard in apps/web/lib/kanban.ts).
A visitor who is not signed in must still provide a name and a valid email to
create a card — see
set_kanban_public_editing — but that
guest-typed address stays on the card row only; it is not captured into the
contact book (createPublicCard captures the session actor's email, not the
typed field).
Capture semantics
- First capture wins. The insert is
ON CONFLICT DO NOTHINGon(project_id, email)— a repeat submission never overwrites a name the project owner may have edited. - Names are never guessed. A missing name stays empty; it is never derived from the email local part, so a nameless capture shows as the bare address, not as "jdoe".
- Gender is never inferred. Auto-capture leaves the field empty until an
owner sets it (
supabase/migrations/0081_contacts.sql). - Each row records its source (
formorkanban) and where it came from (the form endpoint, or the board and card).
No API surface yet
Contacts are a dashboard view today. There is no REST endpoint, no MCP tool,
and no export for them — nothing under apps/web/app/api reads
apps/web/lib/contacts.ts, and the MCP tool list
(packages/kontacts/docs-contracts/src/mcp.ts) has no contacts entry. This
describes current behaviour, not a promise either way.