Skip to content
AskFlorence
Main Navigation ArchitectureFlorence AIAgentsMembersAgent PlatformValidationInfrastructure

Appearance

Sidebar Navigation

Overview

Home

Glossary

System Architecture

Consumer & Agent Flow

Florence AI

Overview

Principles

Runtime

Tool surface

Adding a tool

Tool registry

Knowledge: SBC scenarios & CSR

Voice

Evals & observability

Provider risk & portability

Outage playbook

Roadmap

Build plan

Agents

Overview

Workflows & pain points

Members

Overview

Medicaid coverage gap

Carriers

Overview

Marketplaces

Overview

Agency

Overview

Regulations

Overview

Agent Platform

Overview

Auth Architecture

MongoDB Permissioning

Compliance Model

Data Models

Data Sources

Overview

CMS Marketplace API

CMS dependency map

PUF Data

State Subsidies

SBE Ingestion Playbook

SBE State Watchouts + Decisions

CA Phase C/D Playbook

NY Phase C/D Playbook

Validation

Overview

Methodology

APTC Formula

California 2026

New York 2026

CAPS Formula

Scenario Results

Infrastructure

Account Inventory

AWS Setup Runbook

AWS Organizations

CloudTrail

GuardDuty

Security Hub

Config

CloudFront + WAFv2

Data sources & ingest

Phase 4 DNS

Change Log

Vulnerability Management

MongoDB Setup

Access Control

Data Classification

Documentation Hosting

Post-deploy Smoke

Development

Preflight (local CI mirror)

Testing strategy

Compliance

Overview (auditor entry point)

SOC 2 Control Mapping

HIPAA Control Mapping

CMS EDE Appendix A Mapping

Risk Assessment

Encryption Policy

Data Retention Policy

Privacy Impact Assessment

Consent Capture & Versioning

Incident Response Plan

Access Control Policy

Marketing vs. Portal Analytics

Vendor / Subprocessor Register

Dependency Vulnerability Policy

BAA / Compliance Evidence

Compliance-Automation Integration

Compliance-Automation Vendor Evaluation

Penetration Test Reports

Architecture

Portal entry handoff

Mobile app strategy

Deferred architecture decisions

Session cookie architecture

Share flows

Decisions (ADRs)

Index

0001 — Atlas project isolation

0002 — Append-only audit log

0003 — Narrow-scoped Mongo users

0004 — Cross-cluster Atlas PrivateLink

0005 — Delayed-job architecture

0006 — Mongo user simplification

0007 — Terraform owns ECS task def

0008 — E2E testing strategy

0009 — Self-hosted analytics + observability (superseded)

0010 — PostHog HIPAA Cloud (supersedes 0009)

Runbooks

Security Incident Response

Break-Glass Root Login

Onboard Team Member

Offboard Team Member

Atlas user provisioning

Deploy via Terraform (ENG-277)

Rollback via Terraform (ENG-277)

S3 data bucket migration (planned Phase 11)

Access Reviews

2026-Q2 Review

Session log

Index

2026-04-23 — Phase 10 DNS cutover

2026-04-22 — Phase 8 prod AWS mirror

2026-04-22 — Phase 7 Atlas VPC peering

2026-04-22 — Phase 6 CloudFront + WAF

2026-04-21 — Phase 5 staging go-live

2026-04-17 — Atlas staging

Briefs

Index

Member portal plan (ENG-187)

2026-04-16/17 handoff

2026-04-17 Atlas handoff

System briefing (2026-04-17)

Creative AdBundance proposal brief

Creative AdBundance analytics brief

ElevenLabs RN integration research

Policies

Overview

On this page

Consent Capture & Versioning ​

Status: Active. Effective 2026-05-11. Owner: Taha Abbasi (technical implementation) + Asad Khalid (legal / regulatory). Reviewed: Whenever a new email-capture surface ships, or when the privacy policy / terms / consent statements change. Linear: ENG-201 · GitHub: #58

Purpose ​

Every email AskFlorence captures must carry provable evidence of consent: which version of the privacy policy + terms + consent statement the user agreed to, plus IP, userAgent, timestamp, page URL, and opt-in flags. This document is the operating manual for that system.

Required by:

  • GDPR Art. 4(11) / Art. 7 — consent must be a "freely given, specific, informed and unambiguous indication" and the controller must be able to demonstrate consent was given.
  • CCPA / CPRA — disclosure of personal information categories at collection.
  • CAN-SPAM Act — proof of opt-in for commercial email; persistent unsubscribe.
  • TCPA — "prior express written consent" for platform-purpose contact.
  • HubSpot / Salesforce CRM import — both vendors block list imports without per-record statement text + capture metadata.

The non-negotiable property the system must support: given any consent record, we can answer "show me the exact text that this user agreed to on date X" by URL.

Schema ​

Every email-capturing record stores a consent sub-document with this shape, defined and built by src/lib/consent.ts:

ts
consent: {
  versions: {
    privacyPolicy: string;     // e.g. "2026.04"
    termsOfService: string;    // e.g. "2026.04"
    consentStatement: string;  // verbatim text shown to the user
  };
  optIns: {
    platformContact: boolean;
    marketingEmail: boolean;
    marketingSms?: boolean;
  };
  capturedAt: Date;
  ip: string;
  userAgent: string;
  referrer?: string;
  pageUrl: string;
  method: "checkbox" | "submit_button" | "implicit" | "verbal_recorded";
}

The versions.consentStatement field is the literal text shown to the user, not a key. Keys are stored separately (see Statement keys) for downstream filtering, but the text is the legally-meaningful artifact and is frozen into the record at capture time.

Version registry ​

The source of truth for which versions exist lives in src/lib/legal-versions.ts:

ts
export const PRIVACY_VERSION_HISTORY = [
  { version: "2026.04", effectiveDate: "April 17, 2026" },
] as const;

export const TERMS_VERSION_HISTORY = [
  { version: "2026.04", effectiveDate: "April 17, 2026" },
] as const;

The last entry in each array is "current." LEGAL_VERSIONS.privacyPolicy and LEGAL_VERSIONS.termsOfService are derived from those tails — a single edit bumps the registry without two-place drift.

Helpers exported from the same module:

HelperUse case
getCurrentLegalVersions()Return { privacyPolicy, termsOfService } for the current versions
getLegalVersionMeta(doc, version)Resolve { version, effectiveDate } for a known version, or null
isKnownLegalVersion(doc, version)Boolean check
getConsentStatement(key)Type-safe accessor for CONSENT_STATEMENTS[key]

Frozen archive ​

Old versions stay live at /privacy?v=YYYY.MM and /terms?v=YYYY.MM. The page components are thin shells that:

  1. Read searchParams.v (Next.js 16 async-params pattern).
  2. If v is absent → render the current version.
  3. If v matches a known version → render the frozen Body from src/app/privacy/_versions/vYYYY-MM.tsx (or the terms equivalent) and set <meta name="robots" content="noindex,follow">. Canonical always points at the un-suffixed URL (/privacy or /terms) so search engines don't see the archive as duplicate content.
  4. If v is unknown → notFound() (404).
URLRendersRobots
/privacyCurrent versionindex,follow (default)
/privacy?v=2026.04Frozen v2026.04 prosenoindex,follow
/privacy?v=2099.99404—

Frozen version modules (src/app/privacy/_versions/v2026-04.tsx etc.) must never be edited once shipped. They are append-only artifacts. Editing one breaks the "show me what the user agreed to" property for every consent record that points at that version.

Capture surfaces ​

Every API route that captures an email writes a consent sub-document. Grep for captureConsent( to enumerate.

SurfaceAPI routeCollectionstatementKeymethod
Consumer waitlist (home, agents marketing)/api/waitlistagent_waitlist_submissionsagentWaitlistsubmit_button
Plan-interest signup (on /plans/[planId])/api/waitlistagent_waitlist_submissionsplanInterestsubmit_button
Agent discovery survey (partial save)/api/agents/discoveryagent_survey_responsesagentSurveysubmit_button
Agent discovery survey (full submit)/api/agents/discoveryagent_survey_responsesagentSurveycheckbox

Whenever you add a new email-capture surface:

  1. Pick or add the appropriate key in CONSENT_STATEMENTS in src/lib/legal-versions.ts.
  2. Call captureConsent({ req, statementKey, pageUrl, optIns, method }) and store the returned object as the consent field on the inserted/upserted document.
  3. Add a row to the table above in this doc.

Statement keys ​

The keys in CONSENT_STATEMENTS are stable identifiers that decouple the purpose of the statement from its text. When prose changes between versions, the key stays the same — the text inside changes. Consent records freeze the text (not the key), so reverse-lookup from the stored text identifies which statement the user agreed to.

KeySurface
agentWaitlistAgent-side waitlist sign-up flows (home, /agents, /agent-onboarding)
agentSurvey/agent-discovery survey (partial + full submission paths)
planInterestPlan-interest follow-up on /plans/[planId]

Export workflow (for CRM imports) ​

When HubSpot or Salesforce demands a per-record proof-of-consent list, run:

bash
# All consent records, all-time
npx tsx scripts/export/consent-for-crm.ts --collection all > /tmp/consent.csv

# Just waitlist, last 30 days
npx tsx scripts/export/consent-for-crm.ts --collection waitlist --from 2026-04-12 --out /tmp/waitlist-recent.csv

# Just discovery survey, capped at 100 rows
npx tsx scripts/export/consent-for-crm.ts --collection survey --limit 100

The script (scripts/export/consent-for-crm.ts) is read-only — it uses MONGODB_URI (the app_read user), never the write URI. Safe to run against production.

CSV columns:

email, full_name, company_name, consent_captured_at, consent_ip, consent_user_agent,
privacy_policy_version, terms_of_service_version, consent_statement, statement_key,
opt_in_platform_contact, opt_in_marketing_email, opt_in_marketing_sms,
source_page, page_url, referrer, method, source_collection, submission_id
  • consent_captured_at is ISO-8601 UTC.
  • consent_statement is the verbatim text the user saw. This is the field HubSpot/Salesforce want to see.
  • statement_key is the reverse-lookup of the text against CONSENT_STATEMENTS. Empty if the text doesn't match a known key (would only happen for legacy records predating the registry — none today).

Retention ​

Consent records inherit retention from the parent row + an additional rule:

  • agent_waitlist_submissions rows: 6 years from last activity (data retention policy).
  • agent_survey_responses rows: 6 years from collection.
  • The consent sub-document inside each row: 10 years minimum (HIPAA-aligned, EDE-safer) — the sub-document MUST NOT be dropped or redacted before the row itself ages out, even if the user requests deletion of their account. Right-to-erasure requests delete the row outright; partial redaction would break the audit trail.

Cross-link: Data Retention Policy.

Rolling a new version ​

When the privacy policy or terms text needs to change:

  1. Decide the new version string. Use YYYY.MM (e.g. 2026.06). Bumps are content-driven, not calendar-driven — only roll if the prose actually changes.
  2. Create the frozen module. Copy src/app/privacy/_versions/v2026-04.tsx to src/app/privacy/_versions/v2026-06.tsx (or whatever the new slug is). Edit the prose in the new file only. Update version and effectiveDate exports.
  3. Append to the registry in src/lib/legal-versions.ts:
    ts
    export const PRIVACY_VERSION_HISTORY = [
      { version: "2026.04", effectiveDate: "April 17, 2026" },
      { version: "2026.06", effectiveDate: "June 1, 2026" },  // ← new tail
    ] as const;
  4. Wire the new module in src/app/privacy/page.tsx: import it, add an entry to the VERSIONS array.
  5. If the consent statement text changed, update the corresponding key in CONSENT_STATEMENTS. Old consent records still carry the prior text verbatim — that's the point.
  6. Verify both URLs:
    • /privacy renders the new version
    • /privacy?v=2026.04 still renders the old version (now marked "Archived view")
    • /privacy?v=<new-version> renders identical content to /privacy
  7. Update this doc's capture-surfaces table if any new surface was added.
  8. Material change? Email all consenting users before the new version takes effect (handled separately by ops).

Verification ​

To verify the system end-to-end:

bash
# 1. Type-check
npx tsc --noEmit

# 2. Docs build
cd docs && npm run build

# 3. Local dev server — browser checks
npm run dev
# - GET /privacy            → current version, robots default
# - GET /privacy?v=2026.04  → same prose, robots: noindex,follow
# - GET /privacy?v=2026.99  → 404
# - GET /terms             → analogous
# - View-source: <link rel="canonical" href="/privacy">

# 4. Export script (with MONGODB_URI in env)
npx tsx scripts/export/consent-for-crm.ts --collection all --limit 3
# stdout: CSV header + up to 3 rows; stderr: row count summary

Related ​

  • Privacy Impact Assessment — data-flow-level analysis of every email-capture surface
  • Data Retention Policy — retention period for agent_waitlist_submissions, agent_survey_responses, and consent records
  • Agent Platform Compliance — original design intent for the consent capture system
Pager
Previous pagePrivacy Impact Assessment
Next pageIncident Response Plan

AskFlorence Internal Documentation. Not for public distribution.

AskFlorence

Internal Documentation

Access restricted. Not for public distribution.