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

ENG-187 — Member Portal ​

Context ​

The calculator + plan-detail surfaces are live. A member who picks a plan on /plans/[planId] clicks "Continue to enroll" at PlanDetailHero.tsx:118-125 and currently dead-ends in PlanInterestModal — an email-capture placeholder. The member portal is the surface that replaces this dead-end.

The narrow MVP this plan targets:

  1. Collect the full ACA application data set (9 sections per the CMS Marketplace family application + EDE Year 9 technical reference) so a member can submit
  2. Save-and-resume mirror of the patterns already shipped in agent-onboarding/page.tsx and agent-discovery/page.tsx — same StepKey machinery, same ScreenShell, same "your progress is saved" chip, same auto-advance behavior. NO sync to HubSpot for member-portal data — PHI must not leave the AWS BAA boundary. All retention windows comply with docs/security-compliance/data-retention-policy.md
  3. Surface the submission to an approved agent (read access only — full agent portal scope lives in a separate Linear issue; this plan only delivers the read-side seam)
  4. Land the member in the basic portal with their application status visible (submitted / pending agent review / submitted to Marketplace / DMI-pending / active)

Out of scope for this plan (tracked separately):

  • Full agent queue + lead routing + auto-assignment + agent-side audit trail — separate "agent portal" issue
  • Rich Florence integration (the long-term vision is a real-life AI health-insurance agent walking the member through the application step by step, NOT a chat widget bolted onto a wizard; design lands later, after the underlying basic flow is in)
  • DMI/SVI document upload flows, drug + provider coverage check from portal, appointment booking, copay payment, renewals, insurance ID cards — staged Phase E+ once the basic submission loop works
  • EDE-direct submission to FFM APIs (post-CMS approval, indefinite timeline) — Phase F

This is still a multi-quarter program in total but the first deliverable is narrow: member can complete a full application end-to-end, see submitted status in their portal, and an approved agent can see the lead.

Decisions locked this session ​

DecisionDirectionNotes
Wizard shapeTypeform-style, one-question-per-screen, AI-nativeExtend the existing pattern in agent-discovery/page.tsx — StepKey union, buildSteps(state) conditional builder, ScreenShell sticky question header, Typeform-style step machinery. Florence drives the conversational framing on each step.
Account timingFrictionless capture at startEmail captured on the consent/intro screen (the first or second step), associated with an account immediately. Application doc keyed to account_id from creation. SSN is the actual privacy floor — once the user knows SSN is coming, email-up-front is not the friction barrier. Magic link goes out after submission to verify but does not gate the flow.
Florence prominenceSilent right-rail / bottom drawerFlorence is the conversational voice on each question screen (Playfair italic prompts in her voice) AND a collapsible side-panel for free-form questions. NO prominent "talk to Florence" diverging path at entry — the form IS the Florence experience.
EDE scope targetFull Phase 3 EDE coverage from day onePhase B intake handles AI/AN attestations, non-tax-filers, non-spouse-non-child dependents, foster care history, all immigration statuses, pregnancy/students. Heavier Phase B; no second wave.
Mongo user model4-user functional model per ADR 0006ENG-279 (commit 96c6091) supersedes ADR 0003. No new narrow users for /enroll. Tenant isolation enforced at JWT-in-middleware + (Phase 5) Atlas views.
Subdomain split/enroll, /portal, /a/* on app.askflorence.healthPer ENG-280 (commit ceebc7a). Cookie boundary cleanly separates marketing-tier 3P trackers (GA, Meta pixel, etc.) from PHI-tier portal. CSP on portal blocks all 3P trackers. The first-party self-hosted stack (OpenPanel + GlitchTip, ADR 0009) spans BOTH surfaces — under the AWS Org BAA, no analytics-vendor BAA; PostHog is retired.
Conversion attributionServer-side CAPI on submissionfbclid/gclid/UTMs captured on apex, ride URL hop to portal, fire to Meta/Google CAPI from server post-submit with hashed email. Per Creative AdBundance brief.

System architecture ​

Auth + identity ​

  • Account model: one accounts collection, keyed by email (lowercased). Magic-link first session; no password at launch. Mirrors the agent Tier 1 pattern at src/lib/agent-resume-token.ts — HMAC-signed token, single-use, 15-min TTL.
  • Session: HTTP-only cookie pointing at a server-side member_sessions row (mirrors agent_sessions). Idle 30 min, absolute 24 hr. Looser than agent profile because consumers only access their own PHI.
  • Account creation flow: member enters email on intro screen → server upserts accounts doc + creates member_applications doc keyed to account_id → sets session cookie. No verification gate to start. Magic-link verification happens at submission time (verifies email is real before we submit to FFM) and on cross-device resume.
  • Future MFA seam: member_sessions.factors[] array reserved so TOTP can layer in Phase F when post-enrollment surfaces expose claims/payment data (AAL2 territory).

Data model ​

One document per application — member_applications collection. Sub-documents per section because conditional logic across sections (e.g. "show pregnancy field if any member is female of childbearing age") requires reading the whole tree on every page load. Mongo 16 MB ceiling is irrelevant — max realistic application is ~50 KB.

Multi-member access model (forward-compatible, not all built MVP-1): an accounts collection holds one row per natural person, not per application. A family plan has one primary applicant who drives the flow, plus household members. Any adult household member may eventually claim their own login — life events like separation, divorce, or a young adult aging into independent coverage require it. The data model needs to support this from the start even though only the primary's flow control ships at MVP.

The shape:

  • accounts — one row per natural person. Identity: email + (later) verified identity bundle. An account may be:
    • primary on application A (full control: edit any field, submit, manage plan post-enrollment)
    • co-adult on application B (read full application, edit their own personal-info fields, view post-enrollment status — no submission control)
    • dependent-minor on application C (no direct access; parental control via the primary's account)
    • dependent-adult on application D (limited, future — e.g. an adult child still on a parent's plan)
  • member_applications.householdMembers[] — each entry carries relationshipToPrimary, accountId | null (null = no separate login yet), and an accessRole (primary | co_adult | dependent_minor | dependent_adult)
  • The primary applicant's accountId lives on the application doc as primaryAccountId for fast indexing
  • Read authorization: any account that appears as primary OR as a household-member accountId can read the application (with their role-scoped view)
  • Write authorization: only the primary's account can edit-anything; co-adults can edit their own member's personal-info fields only (a future Phase D / "co-adult portal" deliverable)
  • Split/detach flow (future, design-reserved): if a co-adult wants to start an independent primary application (separation, divorce, age-out), the system clones the household-member's fields into a new member_applications doc keyed to their account as primary; the original retains an audit-only splitFrom: applicationId reference. Not built MVP-1, but the data shape supports it without migration
member_applications {
  _id, applicationId (uuid PK),
  primaryAccountId (ref accounts),     // the primary applicant's login
  coverageYear, status,                // draft | submitted_pending_agent_review | submitted_to_ffm | active | abandoned
  selectedPlan { hiosId, premium, aptc, csr, ... },  // snapshot from /plans/[planId] handoff
  calculator { zip, county_fips, income, householdSize, ... },  // URL-param handoff
  sections {
    primaryApplicant, household[], immigration[], income[],
    coverage, sep|null, attestations, planSelection, payment
  },
  householdMembers[] {                  // forward-compat multi-account access
    memberIndex,                        // 0 = primary, 1+ = others
    accountId | null,                   // null = no separate login yet
    accessRole,                         // "primary" | "co_adult" | "dependent_minor" | "dependent_adult"
    relationshipToPrimary,
    canEditSelf                         // bool; false MVP-1, opens up Phase D+
  },
  sectionStatus { primaryApplicant: "complete"|"partial"|"blank", ... },
  consent[],                            // versioned, append-only, per src/lib/consent.ts shape
  identityVerification { vendor, status, referenceId, completedAt },
  florence { conversationId },
  submittedAt, submittedBy { actor, agentId, method },
  ffmStatus { status, dmis[], svis[] },
  splitFrom: applicationId | null,      // for future detach flow audit
  schemaVersion: 1,
  createdAt, updatedAt
}
  • Unique index: { primaryAccountId, coverageYear } — one application per household per coverage year, keyed by the primary
  • Secondary index: { "householdMembers.accountId": 1 } — lets a co-adult login resolve their household applications fast
  • Partial-save validation: per-section Zod schema with .partial(); full-application schema only runs at submit
  • CSFLE field-level encryption on SSN, immigration document numbers, DOB per encryption-policy.md. Phase B requires this live; not optional
  • MVP-1 behavior: every application has exactly one accountId populated (the primary). Other household members exist as data only, with accountId: null. The co-adult login flow + access UI ships later but the schema slots are reserved now so no migration is needed when they do

Mongo users (4-user functional model per ADR 0006, supersedes ADR 0003) ​

No new per-feature narrow users. Per ENG-279 (commit 96c6091), the 10-user narrow-scope approach was rolled back after three silent regressions in one week. Defense-in-depth shifts up the stack: Phase 5 enforces per-tenant isolation via JWT-in-middleware + MongoDB views, not DB users.

Reuse the existing 4 users:

UserScopeUsed by /enroll
app_read (built-in read@askflorence)DB-wide FINDPlan lookup, eligibility re-check, county/zip reads
app_write (built-in readWrite@askflorence)DB-wide readWriteaccounts, member_applications, member_sessions, florence_conversations writes
app_audit_writer (custom role_audit_writer)FIND + INSERT on agent_audit_log onlyAppend every state transition + Florence interaction
app_admin_schema (custom role_admin_schema)Index mgmt on 3 collections (deploy-time only)Schema rollout for new portal collections

app_admin_schema may need its allowlist extended (Atlas role JSON) to include accounts + member_applications + member_sessions + florence_conversations before Phase A's ensure-indexes job runs.

Authorization at write time is enforced by the JWT in middleware (Phase A wires this): the session cookie carries account_id; every PATCH validates that the application doc's account_id matches the session's account_id. The Mongo write user has DB-wide write but the middleware narrows per-request to "your account only." Per-tenant Atlas views (Phase 5) layer an additional read-side guarantee for super-admin/auditor access patterns.

Subdomain split + Routes (ENG-280 marketing/portal separation) ​

Per ENG-280 (commit ceebc7a), the portal lives on a separate subdomain from the marketing site. This is enforced at the browser cookie boundary — a cookie set on the marketing apex cannot follow the user into the portal subdomain. The cut buys clean separation of marketing-tier analytics + 3P pixels from PHI-tier compliance.

SurfaceHostPosture
Marketing apexaskflorence.health, www.askflorence.healthCalculator, /plans/*, landing pages, waitlist. First-party self-hosted analytics (OpenPanel + GlitchTip). 3P tools (GA, Meta pixel, etc.) OK after compliance review (Creative AdBundance precedent)
Portalapp.askflorence.health/enroll/*, /portal/*, /api/enroll/*, /api/florence/*, /api/member/*. The first-party self-hosted stack (OpenPanel + GlitchTip) is the only analytics allowed — it crosses the marketing/portal boundary because it's first-party self-hosted, under the AWS Org BAA, and never sends data to a 3P. All other trackers (GA, Meta pixel, Google Ads, LinkedIn Insight, TikTok Pixel, etc.) are architecturally blocked via CSP. Zero session-recording on PHI tier.
Agent surfaceapp.askflorence.health/a/* (or sibling subdomain — defer to Phase C decision)Agent auth, review queue, application read-only render. Same first-party-only / no-3P posture as portal

Why the first-party stack is the exception: OpenPanel + GlitchTip are self-hosted on our AWS account, under the AWS Org BAA, never relay data to a third party, and don't fingerprint (ADR 0009). The marketing/portal cut is about third-party data egress, not about analytics in general. The first-party stack crossing both surfaces is desirable — it gives us a unified funnel from marketing-tier "viewed plans" → portal-tier "completed enrollment" without exposing PHI to any vendor outside AWS. GA, Meta pixel, and other 3P trackers send data to vendor-owned servers outside our BAA boundary; those stay strictly on apex.

Routing infrastructure: Both subdomains route through the same CloudFront → ALB. ALB listener rules dispatch by Host header. Route 53 points both hosts to the same CloudFront distribution. Cert (ACM) covers both via SAN. CSP header is added at the ALB or CloudFront response-headers-policy layer, scoped to Host = app.askflorence.health.

Handoff across subdomains: When a member clicks "Continue to enroll" on askflorence.health/plans/[planId], the click navigates to https://app.askflorence.health/enroll?plan=[id]&... carrying the household context via URL params (the existing useCalculator().browseAllPlansUrl shape). The sessionStorage af_household_people does NOT survive the cross-origin hop — server-side recompute from URL params is the only path. Phase A's POST /api/enroll/applications must accept URL params alone as sufficient input.

Document this pattern explicitly for the future native mobile app. When iOS/Android lands, the same constraint will apply — the marketing app and the portal app must be considered separate origins/identity boundaries even though they share branding. The mobile app will invoke a /enroll/start deeplink or its own native entry point; either way the constraint is the same: the portal entry endpoint must accept a self-contained payload (URL params or POST body) and re-derive eligibility server-side. Phase A's POST /api/enroll/applications is the canonical entry point for ALL future surfaces (web, iOS, Android, agent-on-behalf-of). Land this pattern in docs/architecture/portal-entry-handoff.md (new doc, Phase A deliverable) so the iOS engineer who shows up in 2027 inherits the design intent instead of reverse-engineering it.

Conversion attribution (Creative AdBundance pattern, per docs/briefs/creative-adbundance-analytics-brief.md): click IDs (fbclid, gclid, UTMs) captured client-side on apex into sessionStorage. On /enroll handoff, they ride in URL params (subset) and are stored on the application doc. On submission, the server fires CAPI calls to Meta/Google with hashed email + original click IDs. No client-side pixels on portal. No PostHog. This is the architectural template for any future 3P conversion tooling on the portal.

Member-facing (Next.js app router, server actions for form submits) — all on app.askflorence.health:

  • /enroll — entry, consent intro, email capture, application creation
  • /enroll/[applicationId]/step/[stepKey] — Typeform-style one-question-per-screen wizard. stepKey mirrors StepKey in agent-discovery/page.tsx:267
  • /enroll/[applicationId]/review — full read-only summary + typed-signature + perjury attestation
  • /enroll/[applicationId]/submitted — confirmation + what-happens-next
  • /portal — authenticated member dashboard. First deliverable scope: shows submitted-application status (submitted / pending agent review / submitted to Marketplace / DMI-pending / active). Pre-submission, redirects to in-progress application's resume URL. Drug-coverage check, provider-network check, ID cards, appointments, copays, renewals — all staged later
  • /portal/login + /portal/verify — magic-link entry

Agent-facing (intentionally narrow — full agent portal is a separate Linear issue):

The only thing this plan delivers on the agent side is a read-side surface that lets an approved agent see a submitted application. Queue, routing, lead-assignment, agent-side audit trail (did the agent submit directly or have to interact with the member?), agent activity scoring, broker-of-record tracking — all out of scope here. Build a minimal /a/applications/[applicationId] read-only render that an approved agent can hit; the surrounding queue + assignment scaffolding is reserved for the agent-portal initiative.

  • /a/applications/[applicationId] — read-only application render, approved-agent-gated. The component re-uses the wizard's ScreenShell views in readOnly mode. The "Submit via HealthSherpa" handoff button is wired here as a Phase C deliverable but the surrounding workflow (queue + assignment + outcome tracking) is the agent-portal issue's problem.

API routes (all behind Host = app.askflorence.health):

  • POST /api/enroll/applications — create from plan-select handoff (accepts URL params only — no cross-origin sessionStorage)
  • PATCH /api/enroll/applications/[id]/section/[name] — per-section partial save
  • POST /api/enroll/applications/[id]/submit — validate + transition to submitted_pending_agent_review; fires server-side CAPI conversion event with hashed email + stored click IDs
  • POST /api/florence/message — Phase D, server-side proxy to Anthropic/Bedrock with tool-use
  • POST /api/member/auth/{request,verify,logout} — magic-link flow
  • POST /api/a/applications/[id]/{approve,return} — agent actions, append agent_audit_log

Next.js app structure decision (defer to Phase A kickoff but recommend now): single Next.js app deployed to one ECS service, route gating by host. Routes for marketing (/, /plans/*, etc.) refuse to render under app.* host (middleware 404); portal routes (/enroll/*, /portal/*) refuse to render under apex (middleware 404). Single codebase, single deploy, single ECR image. Alternative — two Next.js apps in a monorepo — is more isolation but doubles deploy infra and complicates the calculator → enroll handoff. Recommend single-app + host-gating middleware.

Sensitive data handling (SSN, immigration docs, DOB, financial info) ​

This needs its own focused design pass before Phase B sections that collect SSN/immigration/payment data land. The MVP plan must answer — and the answers must be documented in docs/security-compliance/sensitive-data-handling-member-portal.md (new doc, deliverable as part of Phase A) and reflected back on the Linear ENG-187 issue:

  1. Storage — SSN, immigration document numbers, DOB at minimum. CSFLE field-level encryption per encryption-policy.md with KMS-CMK-derived data keys. Encryption is enforced at the driver layer (mongocryptd / auto_encryption_options), not at the application layer — meaning even direct Atlas queries by app_write return ciphertext for those fields. Define the key-rotation cadence.
  2. In-transit — TLS 1.2+ everywhere. Internal VPC traffic also TLS (no plaintext on the wire even within our VPC).
  3. Presentation — what does the member see in their own portal once SSN is captured? Two reasonable answers: (a) masked ***-**-1234 everywhere except in the moment of entry, with no read-back; (b) re-prompt for SSN at any moment the member wants to see/edit it (verification step-up). Pick (a) + step-up-on-edit as the default and document the rationale.
  4. Step-up verification before reveal — design the verification challenge for any operation that exposes sensitive data: re-enter password / re-do magic link / re-do TOTP if MFA is on / answer challenge question. This is required for ANY portal read that exposes SSN or full DOB or immigration document numbers.
  5. Logging discipline — application logs MUST never include sensitive field values. Define a deny-list at the structured-logger layer; CI check scans logger calls for the deny-listed property names.
  6. Backup + restore — encrypted backups stay encrypted; restore path requires Atlas admin + KMS-CMK access. Document the dual-control restore procedure.
  7. Egress controls — sensitive fields never appear in: HubSpot sync (already excluded), analytics events (OpenPanel: only sanitized event names + bucketed values, never raw income/PII), email content (SES body templates have no PHI), webhook outbound payloads.
  8. What we present to user vs hide entirely — answer per field. SSN: hide (masked-only). Immigration doc number: hide (masked-only). DOB: show in full (it's already on every form they filled). Income amount: show (member already knows). Address: show. Phone: show. The cut is: anything that maps to a high-risk identity-theft vector goes hidden post-entry.
  9. Audit trail — every sensitive-field read (even by the member themselves) appends to agent_audit_log with event member_sensitive_field_accessed. Auditor can reconstruct: who saw what, when.
  10. Data retention — application data retained per the policy in docs/security-compliance/data-retention-policy.md. Define for member-portal specifically: abandoned drafts (TTL?), submitted-but-rejected applications (retain for FFM re-submission?), active member data (retain through coverage year + 7 years for tax), terminated coverage (retain for re-enrollment year + 7).

The deliverable is the new doc + an updated section on the ENG-187 Linear issue + a plan-of-record on how each Phase B field-collection step hooks into the patterns above. The doc lands BEFORE any code is shipped that writes a real SSN.

Florence integration architecture (deliberately TBD) ​

Florence is not a widget bolted onto a wizard. The long-term vision is a real-life AI health-insurance agent walking the member through the application step by step, helping each step of the way — closer to "you're talking to a knowledgeable agent who happens to be operating a CMS-compliant submission system on your behalf" than to "the chat panel on a SaaS app."

We deliberately leave Florence's UX shape flexible at this plan-of-record stage. Build the underlying basic flow first (Phases A-C). When Florence's design lands, the underlying primitives — per-section Zod schemas, PATCH server actions, tenant-isolated application doc, conversation persistence collection, audit log — are the same. The same member_applications write path serves a human-operating-a-wizard, a Florence-operating-on-behalf-of-the-member, and an agent-operating-on-behalf-of-the-member.

What we lock in now (so future Florence design isn't blocked):

  • New collection florence_conversations, append-only, referenced from member_applications.florence.conversationId. Schema TBD; reserve the slot
  • Server-side proxy pattern: anthropic-api-key and Bedrock role ARN never reach the browser; all LLM calls go through a Fargate-hosted route
  • Tool-use schemas (when authored) use generic ACA-vocabulary property names; PHI lives in values, never schema property names or enums (per the structured-outputs HIPAA constraint)
  • Per-interaction audit-log entry to agent_audit_log

What we deliberately defer:

  • Whether Florence is a side-panel, full-takeover chat, voice-first, multimodal — open
  • Whether the wizard "wraps around" Florence or Florence "wraps around" the wizard — open
  • Whether members get a choice of mode or Florence is the default — open
  • How Florence handles failure modes, confidence thresholds, ambiguity, fallback to structured prompts — open
  • Conversation persistence schema beyond "append-only doc per conversation" — TBD when the UX design lands

Agent-assist seam (deliberately minimal — full agent portal is a separate issue) ​

The seam this plan delivers is narrow: when a member submits, an approved agent must be able to see the lead.

  • Member submits → application doc transitions to status: submitted_pending_agent_review
  • SES notification to agents@askflorence.health group inbox (Phase C; agent-side assignment + per-agent routing is the agent-portal issue's scope)
  • An approved agent (gated by the same Tier-1 / Tier-2 auth the agent platform ships) can hit /a/applications/[applicationId] and see the read-only render

Out of scope here: agent queue UI, per-agent assignment + routing, "did the agent submit directly or interact with the member" tracking, broker-of-record bookkeeping, agent activity scoring, full audit of agent actions beyond the basic agent_audit_log entry on read access. All of those land in the agent-portal issue.

Submission abstraction (lock in now, implement later): submitApplicationToFFM(applicationId, strategy) with strategy: "agent_handoff_healthsherpa" | "agent_handoff_healthcaregov" | "ede_direct". Same UI flow in all worlds; only the submission target swaps. The strategy is invoked from the agent surface (the agent-portal issue owns the button + workflow); ENG-187 just stubs the function signature + the underlying data shape it needs.

Phasing ​

Each phase is 1-3 PRs with a defined seam. Code can be written + reviewed before AWS cutover; no PHI write deploys until AWS migration is complete (per #47) AND CSFLE is live AND #56 narrow Mongo users are provisioned.

Phase A — Foundation + subdomain ​

Ships:

  • Infra: app.askflorence.health provisioned (Route 53 + ACM SAN + CloudFront cache behavior + ALB listener rules dispatching by Host header). CSP response-headers-policy on portal Host: script-src 'self' <first-party-analytics-host>; connect-src 'self' <first-party-analytics-host> (= self-hosted OpenPanel + GlitchTip); no GA / Meta pixel / Google Ads / LinkedIn / TikTok / any other 3P tracker host in the allowlist
  • Mongo: accounts + member_applications + member_sessions + florence_conversations collections via app_admin_schema (allowlist extended to these 4 names). Indexes per the data-model section
  • Auth: magic-link send via src/lib/email.ts; session cookie scoped to app.askflorence.health; JWT middleware enforcing account_id match on every PATCH
  • App: /enroll entry route, consent intro, email capture, application creation. Empty /portal shell. Marketing/portal host-gating middleware (refuses to render mismatched routes)
  • Cross-origin handoff: PlanDetailHero.tsx:123 rewired to navigate to https://app.askflorence.health/enroll?plan=[id]&{calculator URL params}. Click IDs (fbclid, gclid, UTMs) piggyback in the URL
  • Server endpoint: POST /api/enroll/applications accepts URL params alone (no cross-origin sessionStorage); server re-derives eligibility via fetch-plans.ts if needed
  • One stub Typeform step (identity name) wired through PATCH server action so the infra is end-to-end

Seam: StepKey enum + buildSteps(state) + ScreenShell + PATCH server action all in place. Adding a section becomes: (1) extend StepKey union, (2) add Zod schema, (3) add ScreenShell-wrapped form component, (4) add to buildSteps.

Blocking deps: #55 privacy/terms publish; #58 consent versioning (adds MEMBER_APPLICATION_INTAKE key to src/lib/legal-versions.ts). #56 is no longer a dependency — superseded by ENG-279/ADR 0006. OpenPanel portal-tier event stream (per ENG-280 timeline mid-May → mid-June) should be wired before Phase B opens to prod.

Phase B — Full Typeform intake (Phase 3 EDE scope) ​

Ships: all 9 sections, every conditional branch, including AI/AN, non-tax-filer, exotic dependent flows. Each step models on agent-discovery + agent-onboarding patterns (same StepKey machinery, ScreenShell, "Your progress is saved" chip, auto-advance for pre-filled steps, conditional follow-up steps). Optimistic save on blur (debounce 500ms). Save-and-resume via magic link. Mobile takeover (lifecycle pattern from LandingCalculator.tsx takeover-bar). ID verification vendor adapter (vendor TBD — same vendor as agents preferred). CSFLE encryption on SSN/DOB/doc-numbers. Sensitive-data-handling doc (per the section above) is a Phase B blocker. No HubSpot egress for any member-portal data.

Phase B also captures the "additional enrollment questions" surface within ENG-187 scope (not a sibling issue): the calculator is deliberately minimal (ZIP, ages, income, household size) but enrollment must ask everything the FFM needs PLUS the inputs that affect pricing the calculator skipped. Most notably tobacco surcharge per member — the calculator does not model this; the enrollment app must collect it and re-price the selected plan before submission. The Section 8 (Plan selection + APTC allocation) screen is the natural place: when the tobacco answer changes, the displayed premium updates in real time, and the final submitted price reflects the surcharged rate. This becomes a sub-deliverable of Phase B, not a separate Linear issue.

Seam for C: completed application docs in submitted_pending_agent_review state. Agent surface reads them.

Phase C — Agent read access (narrow; full agent portal lives elsewhere) ​

Ships: /a/applications/[id] read-only render gated by approved-agent auth. SES notification on submission to the agents group inbox. agent_audit_log insert on agent's first view of the application. Out of scope: queue UI, per-agent assignment, lead-routing logic, "Submit via HealthSherpa" handoff workflow, agent activity tracking — all reserved for the agent-portal issue. The seam Phase C provides is "an approved agent can see a submitted lead." Everything else is the agent-portal initiative's problem.

Adjustable scope: anticipate that the agent-portal issue may want to restructure how applications surface to agents (different URL, different listing, etc.). Keep the read-only render component independently composable so it slots into whatever the agent-portal flow ends up being.

Blocking deps: agent auth Tier 1/Tier 2 live (separate issue).

Phase D — Member-side application status surface ​

Ships: authenticated /portal lands the member on a status view of their submitted application. Status enum surfaced: submitted_pending_agent_review, submitted_to_ffm, dmi_pending, svi_pending, active, denied. Simple editorial card per submitted application. No DMI/SVI document upload yet, no drug coverage check, no provider check, no ID card — just status. This is the deliverable that closes the loop on "member fills application and is in portal where they can see status."

Seam for E: status state machine is live; further surfaces extend the dashboard, not replace it.

Phase E — DMI/SVI dashboard, document upload, lifecycle features (later) ​

Ships eventually: DMI mismatch detail views; SVI verification tracker; document upload (S3 presigned, KMS-encrypted, per encryption-policy.md); drug formulary check per active plan; provider/network check; ID card view; copay payment; renewals. Each becomes its own focused effort on top of the Phase A-D foundation. Florence layers in here in earnest as the rich integrated experience (not a widget).

Phase F — EDE-direct submission (post-CMS approval, indefinite timeline) ​

Ships eventually: server-side FFM API call replaces the agent-handoff strategy in submitApplicationToFFM. Agent-review step retained as a SOC 2 + EDE Year 9 control even when submission is automated.

Information architecture — Typeform-style flow ​

The wizard is one question per screen. Each screen extends the pattern at agent-discovery/page.tsx:267:

  • Sticky top strip (40 px) — lantern mark + AskFlorence wordmark + "Save & exit" tertiary text-link. Mobile-takeover (no nav, no footer)
  • Plan context bar below — issuer + plan name + price; expand-inline accordion on mobile, sticky right-rail card on desktop ≥ 1024 px
  • Progress chip — "Chapter 2 of 4 — Income & coverage" + 9-dot subprogress rule
  • ScreenShell body — eyebrow + Florence's question in Playfair italic + helper text in Inter + structured input + "Saved" fade caption on blur
  • Sticky bottom CTA — "Continue" (ink/white, 4px radius, safe-area-inset padding-bottom)
  • Tertiary "Ask a question about this" link — opens Florence side panel scoped to this step

Chapter wrapping (so the member sees structure, not 30+ atomic screens):

  • Chapter 1. About you — identity, household composition, citizenship/immigration
  • Chapter 2. Income & coverage — income sources, deductions, current/offered coverage, SEP (only outside open enrollment)
  • Chapter 3. Confirm & sign — attestations grouped into 3 editorial cards, APTC + tobacco
  • Chapter 4. First premium — payment

Conditional branches per the Phase 3 EDE scope all flow through buildSteps(state) — citizenship-status answer pushes/pops immigration screens; coverage-section answer pushes/pops employer-coverage screens; outside-OE date pushes SEP screens; etc.

Mobile — full-viewport takeover identical to the calculator pattern. Sticky bottom CTA pinned with padding-bottom: calc(14px + env(safe-area-inset-bottom)). Native keyboard hints: inputmode="numeric" for SSN/EIN/$/DOB, autocomplete tokens throughout, type="tel" for phone, type="email" for email.

Visual register — extends docs/design-system.md: cream paper (--af-paper #FBF6EB) backgrounds; soft-inset cream-on-paper inputs; Playfair italic-gold accents on Florence's question phrasing; 4px radii; no rounded pills; no em-dashes. Per-step composition lives at a new src/app/enroll/[applicationId]/step/[stepKey]/page.tsx with src/app/enroll/enroll.css for af-enroll__* namespace.

Critical files to extend (not duplicate) ​

Handoff seam to flip:

  • src/components/plans/detail/PlanDetailHero.tsx:118-125 — flip "Continue to enroll" from setInterestOpen(true) to navigation to /enroll?plan=[id]
  • src/components/plans/detail/PlanInterestModal.tsx — retire on the same PR that ships Phase A
  • src/lib/hooks/use-calculator.ts — add enrollUrl builder sibling to browseAllPlansUrl

Pattern sources to extend:

  • src/app/agent-discovery/page.tsx — canonical Typeform-style step machinery. Lift StepKey union, buildSteps(state), ScreenShell, SECTION_FOR_STEP, validateStep, sticky question header, "your progress is saved" chip, auto-advance pattern
  • src/app/agent-onboarding/page.tsx — repeater pattern reference
  • src/lib/agent-db.ts — narrow-scope Mongo user with env-var fallback chain (the canonical reference)
  • src/lib/agent-resume-token.ts — HMAC token pattern
  • src/lib/consent.ts + src/lib/legal-versions.ts — consent capture; add MEMBER_APPLICATION_INTAKE key
  • src/lib/email.ts + src/lib/email-template.ts — SES send + RFC 8058 unsubscribe (reuse for magic-link send + submission confirmations)
  • src/app/api/waitlist/route.ts — route handler shape (consent capture + rate limit + idempotent upsert + audit-log)
  • src/app/_home/components/LandingCalculator.tsx — mobile takeover lifecycle (position: fixed, html.style.overflow = "hidden", history.pushState sentinel, focus trap, Escape handler)

Architecture references:

  • docs/adr/0002-append-only-audit-log.md
  • docs/adr/0006-mongo-user-simplification.md — supersedes ADR 0003; 4-user functional model
  • docs/briefs/SESSION_BRIEF_ENG-279_mongo-user-simplification.md
  • docs/briefs/creative-adbundance-analytics-brief.md — subdomain split + server-side CAPI pattern
  • docs/security-compliance/marketing-vs-portal-analytics.md — ENG-280 control doc
  • docs/agent-platform/auth.md — Tier 1 magic-link pattern to mirror
  • docs/briefs/BRIEF_FLORENCE_AI_ARCHITECTURE.md
  • docs/security-compliance/ede-control-mapping.md
  • docs/security-compliance/encryption-policy.md
  • docs/security-compliance/data-retention-policy.md

Research brief (already on disk):

  • ~/.claude/plans/eng-187-member-portal-it-s-squishy-prism-agent-a6787992491f05651.md — 9-section data scope, HealthSherpa flow narrative, AI form-fill guidance, source URLs

Risks + open items ​

  1. HealthSherpa partnership timeline — Phase C agent submission depends on HealthSherpa partner relationship. Mitigation: abstraction takes strategy param; falling back to healthcare.gov broker portal is a config change
  2. Schema migrations once members have draft applications — partial-state save shape is expensive to change in flight. Mitigation: spend Phase A time on per-section Zod schemas; version every doc with schemaVersion: 1
  3. Florence conversational ACA pre-fill is unproven — no public reference implementations. Mitigation: Phase D ships as overlay assist, not replacement. Iterate based on usage
  4. EDE approval timeline (CMS) opaque — 6-18 month variance. Mitigation: Phase C handoff architecture works indefinitely; EDE-direct is an optimization
  5. PHI deploy discipline — every PHI write extends the SOC 2 + EDE audit window backward. We are fully on AWS now; the cutover-era warning about "no PHI on the legacy host" is no longer relevant. Hard rule today: PHI writes only deploy when the sensitive-data-handling doc is signed off + CSFLE is live + OpenPanel portal-tier event stream is live
  6. ID verification at the wrong moment kills conversion — front-loading at /enroll entry bleeds funnel; back-loading discovers failure after the work is done. Mitigation: verify async at end of identity step; if it fails, surface recoverable retry before they leave Chapter 1
  7. Cross-origin handoff fragility — sessionStorage cannot cross from askflorence.health to app.askflorence.health. Mitigation: Phase A POST /api/enroll/applications accepts URL params only and re-derives eligibility server-side via existing fetch-plans pipeline. Document the cross-origin behavior so future surfaces don't regress
  8. Pre-account email-capture posture — capturing email before SSN means we have member email at high partial-completion rates. Implication: ANY abandoned application is a marketing-eligible record (with consent capture). #59 unsubscribe flow must land before Phase A goes to prod
  9. Authorization-in-middleware is the only tenant guard at Phase A — without per-feature Mongo users (per ADR 0006), every PHI read/write rides DB-wide app_write. Middleware bug = cross-tenant leak. Mitigation: (a) every PATCH/GET on /api/enroll/* and /api/portal/* validates session.account_id === doc.account_id before touching the doc; (b) Phase A includes a "tenant isolation" test suite (e.g. session for account A cannot read application B); (c) Phase 5 layers Atlas views as a second-line guarantee for super-admin/auditor read paths
  10. CSP misconfiguration on portal — the architectural assumption "no 3P scripts on portal" is enforced by a Content-Security-Policy header. If it's set as report-only or scoped incorrectly, pixels can sneak in via component imports. Mitigation: Phase A ships the CSP in enforcing mode from day one; Phase B adds a CI check that scans portal-route source files for <script src= / window-namespace pollution
  11. Conversion attribution loss on subdomain hop — fbclid/gclid/UTMs captured on apex must ride the URL hop intact. Long URLs may be truncated or stripped by proxies. Mitigation: the Creative AdBundance brief at docs/briefs/creative-adbundance-analytics-brief.md is the spec; Phase A wires it; verify in staging with a full ad-platform → calculator → /plans → /enroll round-trip
  12. OpenPanel portal-tier event stream not live yet — per ENG-280, OpenPanel self-hosted is mid-rollout (mid-May → mid-June 2026). Phase A cannot ship to prod without a working analytics path on portal. Mitigation: sequence Phase A code-merge ahead of OpenPanel; gate prod deploy on OpenPanel ready

Critical decisions deferred ​

These don't block plan acceptance but need answers before the corresponding phase:

  • Florence provider — Anthropic Direct API (faster, BAA already likely in motion) vs AWS Bedrock (more plumbing, already partially scaffolded per infra/envs/prod/secrets.tf, and is the long-term Phase 3 destination). Pick when Florence UX design lands
  • Submission target — HealthSherpa vs healthcare.gov broker direct vs both. Picks at agent-portal-issue kickoff, depends on Ian's partner conversation
  • ID verification vendor — Persona / Stripe Identity / Plaid / Veriff. Pick before Phase B; aligning with agent vendor reduces BAA + vendor-register work
  • Geographic launch scope — likely all FFM (federally facilitated marketplace) states based on user direction; confirm at Phase B kickoff
  • Tobacco surcharge + other enrollment-time-only questions — calculator is intentionally minimal; tobacco surcharge and any other inputs that affect plan pricing but aren't captured up-front belong in enrollment. Scoped INSIDE ENG-187 Phase B (not a sibling issue): Section 8 (Plan selection + APTC allocation) collects tobacco use per member and re-prices the selected plan in real time. Audit which pricing-affecting fields the calculator skipped before Phase B build kicks off.
  • Payment method scope — offer a variety (card + ACH + other methods as a baseline). PCI scope expansion is accepted; vendor decision (Stripe / Square / direct bank rails) lands at Phase B kickoff
  • Sensitive-data-handling doc sign-off — the new docs/security-compliance/sensitive-data-handling-member-portal.md (Phase A deliverable) must be reviewed + signed off before Phase B sections touch SSN/immigration/payment data

Verification ​

End-to-end verification per phase:

Phase A:

  • npx tsc --noEmit clean
  • scripts/audit/calculator-baseline-diff.ts → ZERO DIFFS (handoff change must not regress calculator math)
  • Subdomain provisioned: dig app.askflorence.health returns CloudFront; curl -I https://app.askflorence.health/ returns 200; ACM cert is valid; ALB Host-header dispatch verified
  • CSP enforced on portal: curl -I https://app.askflorence.health/enroll includes Content-Security-Policy header that allows OpenPanel's self-hosted host but blocks GA / Meta / other 3P trackers; attempting to inject a <script src="https://www.googletagmanager.com/..."> fails in browser console
  • Host-gating middleware: https://app.askflorence.health/plans returns 404; https://askflorence.health/enroll returns 404 (or redirects to apex equivalent)
  • Cross-origin handoff: click "Continue to enroll" on askflorence.health/plans/[planId] → land on app.askflorence.health/enroll carrying URL params including any click IDs → enter email → application doc visible in Mongo with account_id set + session cookie scoped to app.askflorence.health present
  • Mongo direct query: db.member_applications.findOne({ applicationId }) shape matches Zod schema; account_id populated; click IDs preserved on doc
  • Tenant isolation: session for account A cannot GET/PATCH application B (middleware test suite)
  • SES send for magic-link verified
  • OpenPanel events flow from portal — enroll_entry_viewed, enroll_email_captured, enroll_step_completed (OpenPanel self-hosted spans marketing + portal as a unified funnel; no PostHog, no GA, no Meta pixel on portal)
  • WAF + CloudFront route the new endpoints without 403

Phase B (per section as they land):

  • Manual: traverse a representative scenario (single US citizen W-2; married couple with non-citizen spouse; household with AI/AN child claiming cost-sharing exemption; non-tax-filer; outside-OE applicant with SEP loss-of-MEC)
  • Verify CSFLE: encrypted field comes back encrypted in a non-app-user Atlas query
  • Each section's Zod schema fuzz-tested against the canonical CMS family application PDF fields
  • Mobile takeover lifecycle verified at 393×852 — sticky CTA below safe-area-inset, focus trap, Escape exit

Phase C:

  • Agent receives SES notification within 60s of member submission
  • /a/applications/[id] renders read-only view; HealthSherpa handoff URL constructed; agent_audit_log row appears per state transition
  • audit_reader user can db.agent_audit_log.find() and see the full history

Phase D:

  • Florence side panel opens, sees current application state, can extract a field via tool call, returns to the form
  • Per-field "Florence filled this" affordance renders; one-click revert works
  • florence_conversations doc appended; agent_audit_log rows per Florence interaction
  • PHI absent from tool schema property/enum names (audit by scanning the schemas at build time)

Phase E: DMI/SVI surfaces render from member_applications.ffmStatus; document upload writes to S3 with KMS encryption; presigned URL expires correctly.

Phase F: EDE-direct submission round-trips through FFM stub in staging; agent-review step retained even with automation.

What lands first ​

Phase A breaks into two PR clusters:

PR cluster 1 — Infra (no app code):

  • Terraform: app.askflorence.health Route 53 record + ACM SAN + CloudFront cache behavior with portal-scoped response-headers-policy (CSP) + ALB listener rule for Host = app.askflorence.health
  • Mongo: extend app_admin_schema Atlas role to include the 4 new collection names; ensure-indexes job adds indexes for accounts.email, member_applications.{account_id, coverageYear} unique, member_sessions.token unique, florence_conversations.applicationId

PR cluster 2 — App:

  • PlanDetailHero CTA rewire to https://app.askflorence.health/enroll?plan=[id]&... with click-ID + URL-param payload
  • /enroll entry route (host-gated to portal) + consent intro + email capture + magic-link send (cross-device resume)
  • Host-gating middleware (refuses cross-host route renders)
  • JWT session cookie scoped to portal host + per-request tenant guard on PATCH/GET
  • One stub Typeform step (identity name) + PATCH server action
  • OpenPanel event wiring on portal routes

Deploy gate + flow (note the inversion): code merges to main; we test locally; deploy to prod next to verify; only after approval does it promote to staging. This is the opposite of standard pipelines and is the current rule because staging is shared with YCombinator and cannot regress — we have no customers and aren't launched, so prod is the safe-to-iterate environment while staging stays clean. Production deploy gated on: AWS portal stack live (subdomain provisioned, CSP enforcing, Host-header routing correct) + CSFLE for SSN/DOB/doc-numbers + sensitive-data-handling doc signed off + #55 privacy/terms + #58 consent versioning + #59 unsubscribe + OpenPanel portal-tier event stream live (per ENG-280 timeline). #56 is no longer in this gate (superseded by ADR 0006).

Pager
Previous pageIndex
Next page2026-04-16/17 handoff

AskFlorence Internal Documentation. Not for public distribution.

AskFlorence

Internal Documentation

Access restricted. Not for public distribution.