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

Portal entry handoff — design contract ​

Status: Active design contract. Lives in docs/architecture/. Phase A deliverable per ENG-187.

Audience: any future engineer building a new surface (mobile app, agent-on-behalf-of flow, partner-channel embed, B2B-portal lead-in) that wants to drop a member into the enrollment flow without re-collecting calculator inputs.

The constraint ​

The member portal lives on a different origin from the marketing site:

  • Marketing apex: askflorence.health (and www.askflorence.health)
  • Portal subdomain: app.askflorence.health

This is intentional — per ENG-280 the cookie boundary cleanly separates marketing-tier third-party trackers (GA, Meta pixel, etc.) from PHI-tier portal traffic. CSP on portal architecturally blocks third-party scripts. The marketing/portal split applies even though both surfaces share branding and the user experiences them as one product.

Browser physics: a cookie set on askflorence.health does NOT follow the user when they navigate to app.askflorence.health (different origin). sessionStorage and localStorage are also origin-scoped — they do not cross subdomains.

This constraint applies equally to:

SurfaceWhy it triggers the constraint
Today's web → web link from /plans/[planId] → /enrollCross-origin (askflorence.health → app.askflorence.health)
Future native iOS / Android app → portal deeplinkThe native app and the web portal are separate identity contexts even though they share branding
Future partner-channel embed → portal handoffThe partner site is a different origin entirely
Future agent-acting-on-behalf-of-member → portal pre-fillThe agent surface and the member portal are different identity contexts
Future B2B portal (employer onboarding employees) → member portalDifferent origin

The contract ​

The portal entry endpoint accepts a self-contained payload. Every piece of context the portal needs to start an application must arrive in the request — not in a cookie, not in sessionStorage, not implied by referrer.

Canonical entry point:

POST https://app.askflorence.health/api/enroll/applications

Body (JSON):
{
  // Plan selection (from /plans/[planId])
  "planId": "12345NV0010001",            // hiosId
  "planSnapshot": {                      // optional but recommended; lets us
    "premium": 1051.30,                  // anchor pricing if the plan changes
    "aptc": 1031,                        // between handoff and submission
    "csr": "94",                         // "" if N/A
    "metalLevel": "Silver",
    "issuerName": "BCBS"
  },

  // Calculator context (from the calculator URL params)
  "zipCode": "84094",
  "countyFips": "49035",
  "countyName": "Salt Lake",
  "state": "UT",
  "income": 21000,
  "isMarried": false,
  "householdSize": 2,
  "people": [                            // optional; if absent, server uses size
    { "age": 35, "gender": "male", "isPrimary": true },
    { "age": 30, "gender": "female", "isPrimary": false }
  ],

  // Conversion attribution (optional; per the Creative AdBundance pattern —
  // captured client-side on the marketing apex, ride the URL hop intact, fire
  // server-side CAPI on submission)
  "clickIds": {
    "fbclid": "abc123...",
    "gclid": "def456...",
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "open-enrollment-2026"
  },

  // Entry channel (drives future analytics + funnel attribution)
  "entryChannel": "web_marketing"        // "web_marketing" | "ios_native" |
                                          // "android_native" | "agent_assisted"
                                          // | "partner_embed" | "b2b_portal"
}

Response (200):
{
  "applicationId": "uuid-v4",
  "resumeUrl": "https://app.askflorence.health/enroll/<applicationId>",
  "expiresAt": "2026-08-12T00:00:00Z"   // 90-day pre-auth TTL
}

The server then either:

  1. Already has an account for the captured email? — links application to existing account, sends magic-link email if active session not present
  2. No account yet? — application doc created with primaryAccountId: null, anonymous resume token cookie set. Account is created when email captured on the consent intro step (or at submission)

Every consumer of this endpoint — web today, iOS tomorrow, partner-channel after — uses the same contract. The server is the single source of truth on application creation semantics.

Web → web (today's implementation) ​

The marketing-side /plans/[planId] page wires the "Continue to enroll" CTA as a navigation:

ts
const handleContinueToEnroll = () => {
  const params = new URLSearchParams({
    plan: plan.id,                        // hiosId
    zip: form.zipCode,
    county_fips: countyFips,
    county_name: countyName,
    state,
    income: String(income),
    married: String(isMarried),
    household_size: String(householdSize),
    aptc: String(Math.round(aptc)),
    csr,
    fbclid,                              // if present in client storage
    gclid,
    utm_source, utm_medium, utm_campaign,
    entry_channel: "web_marketing"
  });
  window.location.href =
    `https://app.askflorence.health/enroll?${params.toString()}`;
};

Portal-side /enroll page reads searchParams (Next.js server-component prop) and POSTs to its own /api/enroll/applications route, which creates the doc + sets the pre-auth cookie and renders the consent intro screen.

Why URL params and not POST from the marketing page? Because the click is a top-level navigation, not a fetch. Posting requires either (a) a form submit (which works but introduces a redirect step), or (b) the marketing page calls a fetch to portal first (which fails due to CORS preflight + cookie boundaries). Top-level GET with URL params is the only clean pattern.

Native mobile → portal (future) ​

When iOS / Android lands, the mobile app uses one of two patterns:

Pattern 1: deeplink to web portal (simplest, ships first). Native app POSTs to /api/enroll/applications over HTTPS, receives resumeUrl, opens it in an in-app webview or system browser. The webview shares no identity context with the native app — the portal is its own auth domain.

Pattern 2: native portal (later, if needed). Native app implements the full enrollment flow with native UI, calling /api/enroll/applications/[id]/section/[name] PATCH endpoints directly. Same API contract; different presentation layer. Auth becomes app-level (member email + device-bound credential).

Both patterns use the same POST /api/enroll/applications to start. Do not invent a "native-only" entry endpoint — that creates two sources of truth and drifts.

Agent-acting-on-behalf-of (future) ​

Agent surface (separate Linear issue, full agent portal) initiates an enrollment for a member they're helping. The agent UI calls:

POST https://app.askflorence.health/api/enroll/applications

Body adds:
{
  ...same payload as web...,
  "actingAgent": {
    "agentId": "...",
    "memberEmail": "...",                // the member, not the agent
    "consentToken": "..."                // proof the member signed off on the agent helping
  },
  "entryChannel": "agent_assisted"
}

The application doc is created with primaryAccountId set to the member's account (matched by email or created if new). The agent NEVER becomes the primary account on the application — they're the helper. The application is the member's; the agent is recorded in submittedBy.agentId at submission time.

Partner-channel embed (future hypothetical) ​

A B2B partner site (e.g. a state-level navigator, an employer benefits page, a low-income clinic referral) wants to drop a user into AskFlorence with their data pre-collected. Same contract — partner site POSTs to /api/enroll/applications with the pre-collected fields + a partnerChannelId + entryChannel: "partner_embed". We return a resumeUrl; partner site does a window-location navigation or a click-through. Same identity-context discipline as native mobile.

A partner-channel auth token mechanism is the only addition needed here — TBD when a real partner channel exists.

What this contract guarantees ​

  1. No surface depends on cross-origin storage for the handoff. Cookies, sessionStorage, localStorage — none of those are assumed to traverse origins
  2. Every entry path uses the same server endpoint so business logic (eligibility re-compute, dedup against existing applications, consent capture, audit-log entry) lives in one place
  3. Conversion attribution survives the handoff because click IDs ride in the payload, are stored on the application doc, and are forwarded server-side on submission to ad-platform CAPI endpoints
  4. Future surfaces don't reverse-engineer this — they read this doc, see the contract, implement the same shape

What this contract explicitly does NOT cover ​

  • The auth model for the member once they're inside the portal — that's covered in ENG-187 under the "Auth + identity" plan section
  • The Florence integration shape — TBD per the ENG-187 plan
  • The agent-portal initiative's full design — separate Linear issue
  • The retention model for the application doc — see data-retention-policy.md and the sensitive data handling doc

Versioning ​

This contract is versioned via the schemaVersion field on the response. v1 = today's shape. Breaking changes (renaming required fields, removing fields, changing types) bump to v2 and require a 90-day deprecation window during which both versions are accepted.

Pager
Previous pagePenetration Test Reports
Next pageMobile app strategy

AskFlorence Internal Documentation. Not for public distribution.

AskFlorence

Internal Documentation

Access restricted. Not for public distribution.