Kontacts docs

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 / sender key 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 NOTHING on (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 (form or kanban) 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.

On this page