Appearance
Feature map
Every user-facing capability of the portal, with the code that implements it and the checks that cover it. The IDs F01-F17 are fixed: the same IDs appear in the end-to-end runner (steps named E2E-F03 ...), in the Testing page and in review notes. For the rules behind these features see Product primitives.
How to read the columns:
- Unit/integration tests: automated tests that exist today, in
test/*.ts(Vitest with the Workers pool),tests/cloud/*.pyandtests/skills/test_securus_emessage_skill.py(pytest).nonemeans no automated test; that feature is checked by the end-to-end run or by hand. - E2E: the step id the end-to-end runner uses for the feature. A step may report
SKIPwith a reason (for example F05 needs a real browser). - Status: result of the verification run on 2026-10-31; details in each feature's
Verification:line.
| ID | Feature | Surface | Unit/integration tests | E2E | Status |
|---|---|---|---|---|---|
| F01 | Landing page | / | none | E2E-F01 | Verified |
| F02 | Legal & error pages | /terms/, /privacy/, 404 | none | E2E-F02 | Verified |
| F03 | Sign up | /signup/, POST /api/auth/signup | test/api.test.ts (2) | E2E-F03 | Verified |
| F04 | Log in, log out, sessions | /login/, /api/auth/*, /api/me | test/api.test.ts (4) | E2E-F04 | Verified |
| F05 | Safe post-login redirect | public/console/auth.js | none | E2E-F05 | Verified |
| F06 | Console overview & setup stepper | /console/#overview | /api/me shape only (test/api.test.ts) | E2E-F06 | Verified |
| F07 | Inmate management | #inmate, /api/inmate | test/api.test.ts (1) | E2E-F07 | Verified |
| F08 | Plan catalog | GET /api/plans, landing, #billing | test/api.test.ts (1) | E2E-F08 | Verified |
| F09 | Checkout | POST /api/billing/checkout | test/api.test.ts (2) | E2E-F09 | Partly verified |
| F10 | Billing portal | POST /api/billing/portal | test/api.test.ts (1) | E2E-F10 | Partly verified |
| F11 | Stripe webhook sync | POST /api/stripe/webhook | test/stripe_webhook.test.ts (6) | E2E-F11 | Verified (simulated events) |
| F12 | Monthly allowance window & usage view | src/period.ts, GET /api/usage, #usage | test/period.test.ts (5), test/api.test.ts (2) | E2E-F12 | Verified |
| F13 | Hermes link API | /api/hermes/* | test/stripe_webhook.test.ts (3), test/api.test.ts (1) | E2E-F13 | Verified |
| F14 | Hermes bridge cron | hermes_bridge/portal_sync.py | tests/cloud/test_reform_assistant_portal_sync.py (22) | E2E-F14 | Verified (stub Securus) |
| F15 | Securus portal-mode contact add | securus_poc_contacts.py | tests/skills/test_securus_emessage_skill.py (5) | E2E-F15 | Verified (stub + unit) |
| F16 | Developer docs site | docs/ (VitePress) | none (npm run docs:build) | E2E-F16 | Verified |
| F17 | Cloudflare deployment | wrangler.jsonc, docs | none | npm run e2e against production | Verified |
F01 Landing page
- User story: As a family member, I see what the service does, what it costs, and how to start, on one responsive page.
- Entry points: UI
/. Header anchors/#how,/#pricing,/#faq; "Log in" goes to/login/; "Get started" goes to/signup/. - Code files:
public/index.html(sections: hero, "What they get", "How it works", "Pricing", "FAQ", closing call to action),public/assets/site.css,public/assets/site.js,public/assets/hero.svg,public/favicon.svg. - Automated tests: none. Pricing numbers are static HTML and must equal
GET /api/plans(checked byE2E-F08). - E2E steps:
E2E-F01.
Verification: Verified 2026-10-31: npm run e2e against wrangler dev (3 consecutive runs, 15 pass / 0 fail) (E2E-F01); browser QA at 1440px and 390px — See pricing anchor, pricing Choose links carry ?plan=monthly|quarterly|annual, mobile menu opens and shows Pricing.
F02 Legal & error pages
- User story: As a visitor, I can read the terms and privacy pages, and a wrong URL shows a friendly 404 with a way home.
- Entry points: UI
/terms/,/privacy/; any unknown path (Workers Static Assetsnot_found_handling: "404-page"). Unknown/api/...paths return JSON404 not_found, not the HTML page. - Code files:
public/terms/index.html,public/privacy/index.html,public/404.html,wrangler.jsonc(assets),src/index.ts(routeApi). - Automated tests:
test/api.test.ts"reports usage and returns correct route errors" covers the JSON404 not_foundand405 method_not_allowedfor API paths. The HTML pages have none. - E2E steps:
E2E-F02.
Verification: Verified 2026-10-31: E2E-F02 (terms, privacy, 404 headings); browser check of the 404 page.
F03 Sign up
- User story: As a family member, I create an account with email, name and password and land logged in.
- Entry points: UI
/signup/; APIPOST /api/auth/signup(201 {user}, setsra_session;400 invalid_field,409 email_taken). - Code files:
public/signup/index.html,public/console/auth.js,src/index.ts(signup),src/auth.ts(createPassword,createSession),src/http.ts. - Automated tests:
test/api.test.ts: "signs up, returns the current account, logs in, and logs out"; "rejects duplicate normalized email addresses". - E2E steps:
E2E-F03.
Verification: Verified 2026-10-31: test/api.test.ts; E2E-F03 (201 + cookie, 409 duplicate, 400 short password); browser QA — signup with ?plan=quarterly lands on /console/#billing, without a plan on /console/.
F04 Log in, log out, sessions
- User story: As a returning user I log in, stay logged in for 30 days, and can log out; a wrong password gives the same answer as an unknown email.
- Entry points: UI
/login/; APIPOST /api/auth/login,POST /api/auth/logout,GET /api/me. - Code files:
public/login/index.html,public/console/auth.js,src/index.ts(login,logout,me),src/auth.ts(getSessionUser,requireUser,clearSession, expiry cleanup increateSession),src/http.ts(sessionCookie). - Automated tests:
test/api.test.ts: "signs up, returns the current account, logs in, and logs out"; "sets Secure only for HTTPS session requests, independent of APP_URL"; "removes expired sessions for the logging-in user and opportunistically for other users"; "reports usage and returns correct route errors" (401without cookie). - E2E steps:
E2E-F04.
Verification: Verified 2026-10-31: test/api.test.ts; E2E-F04 (HttpOnly, SameSite=Lax, no Secure on http, generic 401, logout 204 then 401); browser QA — Log out returns to /login/, /console/ without a session redirects to /login/?next=%2Fconsole%2F, wrong password shows an error.
F05 Safe post-login redirect
- User story: As a user sent to log in from a deep link, I return to where I was, but a crafted link can never send me to another site; a plan chosen on the landing page carries through signup.
- Entry points:
/login/?next=<path>,/signup/?next=<path>&plan=<id>(planmakes signup finish at/console/#billing;nextis carried to the sibling page). - Code files:
public/console/auth.js(safeNext: only same-origin paths survive; anything resolving to another origin givesnull, falling back to/console/). - Automated tests: none (client-side script).
- E2E steps:
E2E-F05(browser behavior; may reportSKIP).
Verification: Verified 2026-10-31 (browser QA): next=/console/#usage honoured; //evil.example, https://evil.example/, javascript:alert(1) fall back to /console/; /%09/evil.example stays on this origin (it lands on the site's own 404 page, not an external site).
F06 Console overview & setup stepper
- User story: As a signed-in user I see how far along setup is (add inmate, choose plan, connected) and what to do next.
- Entry points: UI
/console/#overview(also the fallback hash); APIGET /api/me(setup.inmate_added,setup.plan_chosen,setup.connected). - Code files:
public/console/index.html,public/console/app.js(overview view, hash router),public/console/console.css,src/index.ts(me). - Automated tests:
test/api.test.ts"signs up, returns the current account, logs in, and logs out" checks the/api/meaccount data; the stepper UI has none. - E2E steps:
E2E-F06.
Verification: Verified 2026-10-31 (browser QA): new account shows "0 of 3 complete", "1 of 3" after adding an inmate; connected account shows the connected banner and "3 of 3 complete".
F07 Inmate management
- User story: As a user I add, edit or remove my one inmate; invalid input is explained; the identity is frozen while a plan is active.
- Entry points: UI
/console/#inmate; APIGET,PUT,DELETE /api/inmate(400 invalid_field,409 inmate_locked). - Code files:
src/inmate.ts,src/index.ts(inmateRoute),public/console/app.js(inmate view),migrations/0001_init.sql(inmates). - Automated tests:
test/api.test.ts"validates inmate fields and locks identity changes while a subscription is active". - E2E steps:
E2E-F07.
Verification: Verified 2026-10-31: test/api.test.ts (validation, inmate-number format, lock while active and past_due); E2E-F07; browser QA — save, field error. Found and fixed during verification: inmate numbers were only checked for non-empty (now ^[A-Za-z0-9-]{3,20}$, fields at most 80 characters), and the lock now also covers past_due.
F08 Plan catalog
- User story: As a visitor or user I see the same three plans and prices everywhere.
- Entry points: API
GET /api/plans(public); UI landing#pricing, console#billing. - Code files:
src/plans.ts,src/index.ts(route table),public/index.html,public/console/app.js(plan grid). - Automated tests:
test/api.test.ts"returns public plans and enforces checkout prerequisites and missing Stripe configuration" (plan ids, prices, months, savings, 40 messages). - E2E steps:
E2E-F08.
Verification: Verified 2026-10-31: test/api.test.ts; E2E-F08 (800/2160/8160 cents, months 1/3/12, 40 messages); landing and #billing show the same prices.
F09 Checkout
- User story: As a user with an inmate I pay for a plan in Stripe Checkout; I cannot double-subscribe or take an inmate someone else pays for.
- Entry points: UI
#billingChoose; APIPOST /api/billing/checkout {plan_id}returns{url}(400 invalid_field,400 inmate_required,409 already_subscribed,409 inmate_taken,503 billing_not_configured,502 stripe_error). Returning from Stripe lands on#billing?checkout=success|cancel. - Code files:
src/billing.ts(createCheckout, previous open session expiry),src/env.ts,public/console/app.js. - Automated tests:
test/api.test.ts: "returns public plans and enforces checkout prerequisites and missing Stripe configuration"; "rejects checkout for a live account subscription and an inmate already subscribed elsewhere". Expiring a previously open session has no automated test. - E2E steps:
E2E-F09.
Verification: Partly verified 2026-10-31: test/api.test.ts and E2E-F09 cover 400 inmate_required and 503 billing_not_configured; browser QA shows the "Payments are not switched on yet" banner. Not verified: a real Stripe Checkout session, open-session expiry, 409 already_subscribed/inmate_taken against Stripe; needs the Stripe sandbox account (see Testing payments).
F10 Billing portal
- User story: As a subscriber I manage payment method, cancellation and plan changes in Stripe's billing portal.
- Entry points: UI
#billingManage billing; APIPOST /api/billing/portalreturns{url}(404 no_customer,503 billing_not_configured). - Code files:
src/billing.ts(createPortal),public/console/app.js. - Automated tests:
test/api.test.ts"returns public plans and enforces checkout prerequisites and missing Stripe configuration" (portal:401without a session,503 billing_not_configured). - E2E steps:
E2E-F10.
Verification: Partly verified 2026-10-31: test/api.test.ts covers 401 without a session and 503 billing_not_configured. Not verified: a real portal session; needs the Stripe sandbox account.
F11 Stripe webhook sync
- User story: As the operator I trust that payment events keep each account's plan, status, billing period and connection correct, exactly once, and that I can replay any event from R2.
- Entry points: API
POST /api/stripe/webhook(Stripe-Signature;400 invalid_signature,400 invalid_event,503 billing_not_configured). Events handled:checkout.session.completed,checkout.session.expired,customer.subscription.created|updated|deleted. - Code files:
src/stripe_webhook.ts(signature check,stripe_eventsidempotency, R2 archivestripe/events/<id>.json, subscription re-fetch, stale and duplicate handling tostripe/duplicates/<id>.json),migrations/0001_init.sql,migrations/0002_checkout_sessions.sql. - Automated tests:
test/stripe_webhook.test.ts: "rejects bad signatures and stale Stripe timestamps"; "accepts signed subscription events, archives and deduplicates replay"; "uses subscription item periods when Stripe omits top-level periods"; "keeps connected renewals and ignores stale events for replaced subscriptions"; "records a cross-account live inmate subscription as a duplicate"; "updates the plan from a changed subscription price even with stale metadata". - E2E steps:
E2E-F11.
Verification: Verified 2026-10-31 with locally signed events: test/stripe_webhook.test.ts; E2E-F11 (create, replay, bad signature, renewal keeps connected, deletion cancels). Not verified: events sent by real Stripe and the re-fetch/cancel calls, which need STRIPE_SECRET_KEY.
F12 Monthly allowance window & usage view
- User story: As a subscriber I see how many of this month's 40 messages are used and left, even on a quarterly or annual plan.
- Entry points: UI
/console/#usage; APIGET /api/usage; librarymonthlyWindowinsrc/period.ts. - Code files:
src/period.ts,src/usage.ts(getUsage),public/console/app.js(usage view). - Automated tests:
test/period.test.ts: "clamps a January 31 anchor to February's final day"; "steps through quarterly billing months and clamps at billing end"; "selects the twelfth annual month and clamps to annual period end"; "uses one complete window for a February-to-March monthly period"; "uses exactly three windows for an April-to-July quarterly period".test/api.test.ts: "returns only the current monthly window usage for quarterly subscribers"; "reports usage and returns correct route errors" (zeros without a subscription).test/stripe_webhook.test.ts"uses subscription item periods when Stripe omits top-level periods" also checks the usage window. - E2E steps:
E2E-F12.
Verification: Verified 2026-10-31: test/period.test.ts, test/api.test.ts; E2E-F12 (window contains now and ends at or before billing end, allowance 40, used 1); browser QA of the Usage view.
F13 Hermes link API
- User story: As the Hermes host I fetch pending connections, acknowledge them, report usage once per message and read each inmate's allowance, authenticated by one bearer token.
- Entry points: API
GET /api/hermes/connections?status=pending,POST /api/hermes/connections/<id>/ack,POST /api/hermes/usage,GET /api/hermes/allowances; all401 unauthorizedwithoutAuthorization: Bearer <HERMES_LINK_TOKEN>. - Code files:
src/hermes.ts,src/usage.ts(recordUsage),src/period.ts,src/index.ts(routeApi). - Automated tests:
test/stripe_webhook.test.ts: "authorizes Hermes connections, acknowledges status, and deduplicates usage"; "returns a quarterly Hermes connection with the current monthly and billing windows"; "returns the current monthly window for quarterly Hermes allowances".test/api.test.ts: "records Hermes usage only for the inmate with a live subscription". - E2E steps:
E2E-F13.
Verification: Verified 2026-10-31: test/stripe_webhook.test.ts, test/api.test.ts; E2E-F13 (401 without token, pending connection with monthly window, allowances).
F14 Hermes bridge cron
- User story: As the operator I run one cron script that connects paid inmates in Securus, refills allowances monthly, revokes when a subscription ends, and reports usage back, safely re-runnable.
- Entry points: script
python hermes_bridge/portal_sync.py [--token-file PATH](envREFORM_PORTAL_URL), run as a Hermes--no-agentcron job; prints a JSON summary and exits non-zero if anything failed. - Code files:
hermes_bridge/portal_sync.py,hermes_bridge/README.md, state file$HERMES_HOME/securus/portal_sync_state.json. - Automated tests:
tests/cloud/test_reform_assistant_portal_sync.py:test_run_cli_is_bounded_noninteractive_utf8;test_pending_connection_adds_contact_grants_allowance_and_acks_connected;test_already_added_contact_is_acknowledged_connected;test_connection_uses_portal_contact_mode_and_grants_plan_allowance;test_timed_out_contact_setup_is_acknowledged_as_failed;test_connection_with_null_period_is_failed_without_being_stored;test_null_allowance_period_is_not_saved_and_later_window_refills;test_period_comparison_recovers_from_invalid_previous_and_rejects_invalid_candidate;test_reconnect_grants_only_unused_allowance_from_matching_record;test_quarterly_reconnect_tracks_monthly_window_and_refills_next_month;test_reconnect_never_grants_negative_unused_allowance;test_reconnect_without_matching_used_value_grants_full_plan_allowance;test_failed_allowance_reactivation_keeps_revoked_marker_until_retry_succeeds;test_refill_occurs_once_for_each_monthly_window;test_missing_allowance_revokes_remaining_stamps_once;test_usage_idempotency_key_includes_ledger_entry_for_repeated_reply_id;test_usage_without_reply_ids_still_posts_once;test_main_exits_nonzero_after_processing_an_operation_failure;test_cli_failure_is_acknowledged_and_later_connections_continue;test_refill_occurs_only_once_for_each_period;test_usage_posts_each_message_once_across_runs;test_token_is_not_printed_by_cron_entry_point. - E2E steps:
E2E-F14.
Verification: Verified 2026-10-31: tests/cloud/test_reform_assistant_portal_sync.py; E2E-F14 runs the real portal_sync.py against the live Worker with a stub Securus skill: connects once, usage posted once across reruns, revokes after cancellation. Not verified against the live Securus account.
F15 Securus portal-mode contact add
- User story: As a paying family, my loved one is added to the Securus service with the plan's allowance already in place when the welcome message arrives, without bothering the service admin.
- Entry points: script
securus_poc.py add_contact ... --portal-allowance N(called by the bridge). - Code files:
skills/communication/securus-emessage/scripts/securus_poc_contacts.py(cmd_add_contact,_set_portal_allowance),skills/communication/securus-emessage/scripts/securus_poc.py. - Automated tests:
tests/skills/test_securus_emessage_skill.py:test_portal_add_uses_plan_allowance_without_admin_message_or_reply_mark;test_portal_already_added_enabled_contact_is_success_without_second_welcome. For contrast, the admin-driven path:test_add_contact_command_welcomes_the_new_member;test_admin_notice_is_only_the_first_name_and_allowance;test_a_contact_added_outside_the_agent_gets_the_default_allowance. - E2E steps:
E2E-F15.
Verification: Verified 2026-10-31: tests/skills/test_securus_emessage_skill.py (portal mode: no admin notice, allowance before welcome, already-added is success, admin path unchanged); E2E-F15 asserts one portal-mode add_contact with allowance 40. Not yet deployed to the Mac.
F16 Developer docs site
- User story: As a developer I can read and build the full documentation, including the product primitives, this feature map, the Stripe section and the reference.
- Entry points:
npm run docs:dev,npm run docs:build,npm run docs:previewfromcloud/reform-assistant/. - Code files:
docs/.vitepress/config.mts(nav and sidebar),docs/index.md,docs/product/,docs/guide/,docs/stripe/,docs/reference/,docs/operations/. - Automated tests: none;
npm run docs:buildfails on broken links and is the check. - E2E steps:
E2E-F16.
Verification: Verified 2026-10-31: npm run docs:build completes with no dead links; served at the Tailscale preview.
F17 Cloudflare deployment
- User story: As the operator I deploy the Worker with its D1 database, R2 bucket, secrets and domain by following the guide.
- Entry points:
npx wrangler d1 create,npx wrangler r2 bucket create,npx wrangler d1 migrations apply reform-assistant --remote,npx wrangler secret put <NAME>,npm run deploy; guide Cloudflare deployment. - Code files:
wrangler.jsonc,migrations/,.dev.vars.example,docs/guide/cloudflare.md,docs/operations/runbook.md. - Automated tests: none;
test/apply-migrations.tsapplies the migrations in the Vitest pool, which exercises the schema but not a deployment. - E2E steps:
E2E-F17.
Verification: Verified 2026-11-01: deployed to https://reform-assistant.phucdoan.workers.dev (D1, R2, secrets, APP_URL) and docs to https://reform-assistant-docs.pages.dev; npm run e2e against production passed 3 consecutive runs (15 pass, 0 fail); browser check on the live site. Found and fixed during deployment: Cloudflare blocks Python's default urllib User-Agent (403, error 1010), so the Hermes bridge now sends its own. Custom domain not set up yet (see Cloudflare deployment).