Files
resolutionflow/docs/plans/archive/2026-03-04-survey-invite-tracking-design.md
Michael Chihlas cbb4b25671
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m42s
CI / e2e (pull_request) Successful in 10m11s
CI / backend (pull_request) Successful in 10m43s
fix(ui): drop setState-in-effect in useAuthSessionExpiry
CI surfaced react-hooks/set-state-in-effect on the synchronous
setState(computeState(token)) inside the useEffect body. The earlier
shape mirrored token -> state via an effect, which is exactly the
"you might not need an effect" pattern React 19's eslint rule now
flags.

Switch to derived state: compute during render, use a useReducer
tick to force re-render on the 30s cadence (so relative timestamps
stay current even when token props don't change). Same observable
behavior, no cascading renders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:15:11 -04:00

3.1 KiB

Survey Invite Tracking — Design

Date: 2026-03-04 Status: Approved

Goal

Add invite tracking to the FlowPilot survey so Michael can create personalized links, optionally email them, and see who has/hasn't responded. Each invite token is single-use — one submission per token.

Data Model

New table: survey_invites

Column Type Notes
id UUID PK
token VARCHAR(32) UNIQUE Random URL-safe token
recipient_name VARCHAR(255) NOT NULL Who it's for
recipient_email VARCHAR(255) NULL Only if emailing
status VARCHAR(20) DEFAULT 'pending' pending or completed
email_sent BOOLEAN DEFAULT false Whether Resend email was sent
created_at TIMESTAMPTZ NOT NULL
completed_at TIMESTAMPTZ NULL Set on submission

Modified table: survey_responses

Add invite_id UUID FK nullable → survey_invites.id. Responses from tokenless /survey have invite_id = NULL.

API Endpoints

Public (no auth)

  • GET /api/v1/survey/invite/{token} — Returns invite status ({ name, status }). If completed, frontend shows "already submitted" screen. Returns 404 for invalid tokens.
  • POST /api/v1/survey/submit — Modified: accepts optional token field. If token provided, validates it's pending, links the response, and marks invite as completed. Returns 409 if token already used.

Admin (super_admin auth)

  • POST /api/v1/admin/survey-invites — Create invite. Body: { recipient_name, recipient_email?, send_email? }. Generates token, optionally sends email. Returns the invite with the full survey URL.
  • GET /api/v1/admin/survey-invites — List all invites with status.

Frontend

Survey page changes (/survey)

  • On load, reads ?t=<token> from URL params
  • If token present: calls GET /survey/invite/{token}
    • If completed → show "already submitted" screen
    • If pending → show survey, include token in submission payload
    • If 404 → show survey without token (treat as open link)
  • If no token: show survey as-is (open access)

Admin page (/admin/survey-invites)

Top section: Create Invite

  • Name input (required) + Email input (optional)
  • "Generate Link" button → creates invite, shows URL with copy button
  • "Send Email" button → creates invite with send_email: true, shows confirmation toast
  • "Send Email" only enabled when email field is filled

Bottom section: Invite Table

  • Columns: Name, Email, Status badge (pending amber / completed green), Sent (email icon or dash), Created date, Completed date
  • Sorted by created_at descending

Email Template

Uses existing EmailService + Resend pattern. Dark-themed email matching Slate & Ice aesthetic:

  • Subject: "FlowPilot Survey — Your expertise matters"
  • Body: Brief intro, CTA button linking to /survey?t=<token>, ~5 minutes note
  • From: existing FROM_EMAIL config

Constraints

  • No token expiration
  • No reminder/resend (keep it simple)
  • Tokenless survey still works for open sharing
  • One submission per invite token (enforced backend + frontend)