Skip to content

Repository files navigation

CareMatch

A verified, budget-aware marketplace for in-home care and household help for older adults.

CareMatch connects older adults (and their families) with verified in-home helpers across a spectrum of services — cleaning and errands, companionship, personal care, and eventually skilled care — matched on care needs, personality, and budget, with an identity-verification trust layer as the headline differentiator.

The product is governed by five design principles: senior sovereignty, trust made visible, budget as a first-class input, one seam-free spectrum, and radical accessibility. See docs/BLUEPRINT.md for the full product & build blueprint and docs/ARCHITECTURE.md for the system design.


What's in this repository today

This repo implements the defensible core of the platform end to end:

  1. Matching Engine and Budget Optimizer (blueprint §4) — the blueprint is explicit that "your defensibility lives entirely in matching quality, trust UX, and accessibility."
  2. Care-plan lifecycle, scheduling, and the visit-event stream (§6) with visit-time identity verification (§3, stage 5) — turning a matched, optimized plan into approved, scheduled, verifiable visits.
  3. Funding Navigator (§4.4) — a decision-tree screener for VA Aid & Attendance, Medicaid HCBS waivers, LTC insurance and Medicare Advantage that re-runs the optimizer against an adjusted effective budget.
  4. Notifications (§2, §5) — turns the visit-event stream into audience-targeted alerts: family reassurance on verified arrival, urgent ops escalation on a "not good" review or a failed identity check.
  5. Real authentication and a real database — role-based accounts (senior/family/provider/ops) with hashed passwords and JWTs, and full persistence in Postgres. No in-memory shortcuts, no runtime mock data.
  6. Consent / permissions (§5.2) — senior-granted, revocable family access at view / book / financial levels, enforced by the authorization layer.
  7. Senior web app (§5.1) — an accessibility-first PWA served by the same server: login → home → one-question-per-screen booking → match card-stack with visible verification → "here is your care plan." This is where "trust made visible" and "radical accessibility" stop being JSON and become a UI.
  8. Family dashboard (§5.2) — a responsive, information-dense web app served at /family: a reassurance strip (last / next visit + verification), visit detail with the event timeline, the shared care plan, and budget — scoped strictly to what the senior granted. The senior invites family by email.
  9. Billing & payments — subscription plans, invoices with line items, the marketplace take-rate, and provider payouts, over a PaymentGateway boundary (in-house ledger locally; swap for Stripe Connect in prod). Money is in cents.
  10. Audit log & ops console — an append-only audit trail of sensitive actions and an ops-only console at /console (platform metrics, an incident queue for failed verifications and bad reviews, and the audit trail). Ops accounts are provisioned from config, never self-registered.
  11. Account lifecycle & compliance — password reset and email verification (hashed, single-use, expiring tokens over a transactional email boundary), and the CCPA/GDPR right to data export and account deletion (scrubs PII and health data, de-identifies retained financial records).
  12. Provider verification & trust (§3) — the moat, made real: a verification record pipeline (identity → background → credential → continuous monitoring) whose results derive a provider's verified_tier and active/suspended status (never self-declared). Matching only surfaces active providers; an adverse result suspends them instantly. The senior app's "Verified helper" badge shows each provider's real, dated stages. Provider profiles are claimed safely (one account per unclaimed profile), and VerificationRecords are recorded via an ops endpoint that stands in for a Persona/Checkr webhook.

It is a running application: every request is authenticated at the gateway, all state lives in the database, authorization enforces senior sovereignty, and a real senior-facing client drives the whole loop.

web/             Senior PWA — index.html, styles.css (design system), app.js
web/family/      Family dashboard — index.html, family.css, family.js
scripts/         Playwright end-to-ends: e2eSenior.ts, e2eFamily.ts
src/
  config.ts      Env-driven runtime config (db, jwt, port)
  main.ts        Entrypoint: open DB → migrate + seed → start HTTP server
  domain/        Core model — tiers, tasks, entities, geo, booking, user, funding
  matching/      Rules-based v1 matching engine (hard filters + weighted scoring)
  optimizer/     Budget optimizer — the "here is your care plan" signature feature
  funding/       Funding Navigator — screens for VA/Medicaid/LTC/MA, adjusts budget
  booking/       Care-plan lifecycle, visit scheduling, visit-event state machine
  notifications/ Turns visit events into audience-targeted alerts + dispatcher
  auth/          scrypt password hashing, HS256 JWT, register/login/authenticate
  db/            PGlite/Postgres client, migrations, seed, and repositories
  api/           App wiring, HTTP server, auth middleware + authorization
  data/          Provider seed fixtures + a representative intake profile
test/            Node built-in test runner suites (65 tests, real DB + auth)
docs/            Blueprint, architecture, data model

Stack. Node ≥ 22.6 (TypeScript run directly via --experimental-strip-types). The database is PostgreSQL: PGlite — real Postgres compiled to WASM, running in-process — locally and in tests, and managed Postgres in production against the identical schema (DATABASE_URL). Auth uses only Node built-ins (scrypt + HMAC-SHA256 JWT). The one runtime dependency is the database driver; the rest is standard library.

What is intentionally not built yet

The blueprint describes eleven backend components, three client apps, and a phone concierge. Deferred (and why): identity/verification vendor onboarding, payments, messaging, the PII vault, and the three client apps — all either depend on third-party vendors (Persona/Checkr, Stripe Connect, Twilio) or build on the core contracts and API defined here. See docs/ARCHITECTURE.md.


Quick start

cp .env.example .env   # set JWT_SECRET; PGLITE_PATH=./.pgdata to persist to disk
npm install            # installs the DB driver (+ typescript/@types/node for dev)
npm run typecheck      # tsc --noEmit, strict
npm test               # 65 tests against a real in-memory Postgres
npm run db:setup       # run migrations + seed the provider roster
npm start              # migrate + seed + serve the API on http://localhost:3000

Then open http://localhost:3000/app — the senior web app. Register as a senior, tap "Get help with something," and walk the booking flow; the plan you confirm is scheduled and shows up on your home screen.

Pure-logic demos that need no server or DB:

npm run demo           # a full match + care-plan run for the sample profile
npm run demo:booking   # propose → approve → schedule → run a visit + notifications

The senior flow is covered by a Playwright end-to-end (opt-in, not part of npm test): npm i -D playwright-core && npm run test:e2e.

Try the API (authenticated)

Every route except /health and /auth/* requires a bearer token.

npm start

# 1) Register a senior — returns { token, user }
TOKEN=$(curl -s -X POST localhost:3000/auth/register \
  -H 'content-type: application/json' \
  -d '{"email":"rosa@example.com","password":"password123","role":"senior","displayName":"Rosa"}' \
  | node -pe 'JSON.parse(require("fs").readFileSync(0)).token')

# 2) Propose a care plan (seekerId is taken from the token, not the body)
curl -s -X POST localhost:3000/care-plans \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{
    "needs": [
      { "task": "bathing", "hoursPerWeek": 3, "preferredDays": ["mon","wed","fri"] },
      { "task": "companionship", "hoursPerWeek": 4, "preferredDays": ["tue","thu"] },
      { "task": "housekeeping", "hoursPerWeek": 3, "preferredDays": ["wed"] }
    ],
    "location": { "lat": 37.7793, "lng": -122.4193 },
    "monthlyBudget": 2800, "budgetHardness": "firm",
    "preferences": {
      "languagesRequired": ["Spanish"], "petsInHome": true, "smokingOk": false,
      "personality": { "chatty": 0.7, "active": 0.5, "structured": 0.8, "playful": 0.6, "nurturing": 0.9 }
    },
    "previousProviderIds": ["prov_maria"], "planKey": "smart_mix"
  }'

POST /match returns ranked, explainable candidates and 2–3 named care-plan options (full_coverage, smart_mix, essentials), each priced against real providers with a recommendedPlanKey.

Senior web app (web/)

An accessibility-first PWA served by the same Node server at /app — no build step, no framework, one runtime dependency total (still just the DB driver). The design system (web/styles.css) treats WCAG 2.2 AA as the floor: 20px base type, ≥56px touch targets, ≥7:1 contrast, always-visible focus, no gesture-only or timed interactions, honoured prefers-reduced-motion, and a persistent "Call us" button in the same corner of every screen.

The screens implement the blueprint's §5.1 senior experience:

  • Home leads with the next-visit card (who is coming, when) and a tappable Verified helper badge that opens "how we verify every helper" — trust made an observable thing, not a claim. Then one big "Get help with something," then "My people" rebook tiles.
  • Booking is one question per screen with progress dots and a back button always in the same place: what you need (plain-language tiles) → which days → budget.
  • Match results are a card stack (one helper at a time, ≤5), each with a face, the verification badge, and "why this match" reasons.
  • "Here is your care plan" shows the recommended plan as a monthly cost against a budget bar; confirming proposes, approves, and schedules four weeks of visits, which then surface on Home.

Verified end-to-end with Playwright (scripts/e2eSenior.ts).

Family dashboard (web/family/, served at /family)

The second client — information-dense where the senior app is sparse (§5.2). A family member signs in and sees only the seniors who have granted them access, at the level granted. The Overview leads with the reassurance strip that answers "is Mom okay and is this working" — last visit (with the verified-arrival time), next visit, and recent activity. Visits drills into the event timeline (en-route → verified arrival → checked out → note → review). Care plan shows the shared weekly plan; Budget shows spend against the plan.

The senior grants access from their app's Share with family screen by email and level; the whole handoff is covered by scripts/e2eFamily.ts (npm run test:e2e:family).

Operations (running as a service)

The API gateway is production-shaped:

  • Health & readiness: GET /health (liveness) and GET /ready (checks the database). Metrics: GET /metrics in Prometheus text format.
  • Security headers on every response (CSP, X-Content-Type-Options, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, HSTS in production), configurable CORS allowlist, and an X-Request-Id echoed for tracing.
  • Rate limiting per client (tighter on /auth/*) → 429 with Retry-After.
  • Structured JSON logs (one line per request: method, path, status, duration, request id) suitable for any log aggregator.
  • Graceful shutdown on SIGINT/SIGTERM.
  • CI (.github/workflows/ci.yml) runs typecheck + tests on Node 22.
  • Docker: docker build -t carematch . && docker run -p 3000:3000 -e JWT_SECRET=… carematch (no build step — TypeScript runs via type stripping; the image ships source + runtime deps and has a HEALTHCHECK).
  • API description: docs/openapi.yaml (OpenAPI 3.1).

Authentication & authorization

  • Accounts have a role: senior, family, provider, or ops. Passwords are hashed with scrypt; login/registration issue an HS256 JWT (Authorization: Bearer <token>).
  • Senior sovereignty is enforced in code: only the senior (or ops acting on their behalf) can approve a care plan or schedule visits; a family member can propose but not approve. A senior only ever sees their own plans and visits.
  • Provider-scoped actions: only the assigned provider can mark en-route, check in, check out, or note a visit. Only the senior can review it; only ops can mark a no-show. The seekerId on any request is derived from the token, never trusted from the body.
  • Consent is senior-granted and granular (§5.2). A family account has no access to a senior until the senior grants a consent at view (see plans, visits, notifications), book (also propose/schedule), or financial (also the funding navigator's money detail). Higher levels include the lower ones; consent is revocable (POST/GET/DELETE /consents), and family can never approve a plan regardless of level.

How the core works

Matching engine (src/matching)

  1. Hard filters remove non-candidates: tier eligibility (a provider's verified tier must cover the highest tier the needs require), service radius, required languages, availability overlap, pets, and smoking.
  2. Transparent weighted-linear scoring ranks survivors over five factors — schedule fit (0.25), budget fit (0.20), personality alignment (0.20), quality signals (0.20), and a continuity bonus (0.15). The weights are config, not code (src/matching/weights.ts) so ops can tune them weekly.
  3. Every candidate carries plain-language reasons ("Within budget · Speaks Spanish · You have worked together before") — explainability is trust for this audience. ML enters only at v3, as a re-ranker.

Budget optimizer (src/optimizer)

Given a needs profile and budget, it decomposes needs into task blocks (each with a minimum required tier from the task catalog) and produces named plans:

  • Full coverage — everything requested, staffed by the fewest, highest-tier providers. Max continuity, max cost; shown honestly even when over budget.
  • Smart mix — every block assigned to the cheapest tier that safely covers it. Lower-tier hours are billed at that tier's market rate, so a personal-care aide doing housekeeping costs household money — the mechanism that makes this plan meaningfully cheaper than paying skilled rates for every hour.
  • Essentials — only safety-critical blocks, sized to come in under budget, with a clear list of what was deferred.

This reframes the product from "search for a caregiver" to "here is your care plan."

Funding Navigator (src/funding)

A guided, content-first eligibility screener (no payer integrations) for VA Aid & Attendance, Medicaid HCBS waivers, long-term care insurance, and Medicare Advantage supplemental benefits. It rates each source likely / possible / unlikely with an indicative monthly benefit and concrete next steps, sums the likely benefits into an estimated funding figure, and re-runs the optimizer at the raised effective budget — so the difference the funding makes shows up in real plans, side by side (assessAndReplan, POST /funding/plan). Every figure is flagged as an estimate, not an eligibility determination.

Once a plan is chosen, it moves through a lifecycle that encodes senior sovereignty and visible trust:

  • Care plan (carePlanService.ts) — the optimizer option is promoted to a versioned CarePlan in the proposed state. Families may propose; only the senior approves, which activates it. Visits can only be scheduled from an active plan.
  • Scheduling (schedulingService.ts) — an active plan expands into concrete Visits over a date range. Same-day, same-provider blocks are co-located into one visit; weekly hours spread evenly across scheduled days.
  • Visit lifecycle (visitLifecycle.ts) — a strict state machine over the VisitEvent stream: scheduled → en_route → verified_arrival → checked_out → noted → reviewed, with cancelled / no_show off-ramps. Check-in requires both a biometric selfie match and a GPS geofence pass — a failed attempt throws rather than advancing, catching the wrong-person / account-sharing case. A "not good" review flags the visit for a same-day human call-back.

Notifications (src/notifications)

Every visit transition is turned into audience-targeted notifications by a pure rules function (notificationRules.ts), then delivered through a swappable NotificationSink (console for the demo/server, a recording sink for tests; one sink per real channel — Twilio, push, email — in production). Highlights:

  • Verified arrival → family reassurance ("✓ Maria verified on arrival, 9:02 AM") — the observable trust event made visible.
  • "Not good" review → urgent ops phone-call escalation, plus a family heads-up — the review doubles as incident detection (§5.1).
  • Failed identity check → urgent ops alert naming the reason (low selfie match / out of geofence). This isn't a visit event — the check-in is rejected — but it's a possible wrong-person signal, so it's surfaced immediately.

BookingService fans these out and persists them as each visit progresses; GET /seekers/:id/notifications returns a senior's inbox.

HTTP endpoints

Method & path Purpose
POST /auth/register Create an account, returns { token, user } (public)
POST /auth/login Exchange credentials for a token (public)
GET /health Liveness check (public)
POST /consents Senior grants a family member access (body: { granteeId | granteeEmail, level })
GET /seekers/:id/visits A senior's visits (view access)
GET /seekers/:id/care-plans A senior's care plans (view access)
GET /billing/plans · GET|POST /billing/subscription Membership plans & subscription
POST /billing/invoices · GET /billing/invoices Generate/list invoices (financial access)
POST /billing/invoices/:id/pay Pay an invoice; settles provider payouts
GET /billing/payouts A provider's payouts
GET /admin/metrics · /admin/incidents Platform metrics, incident queue (ops)
GET /admin/audit · /admin/users Audit trail, user lookup (ops)
POST /auth/password-reset/request · /confirm Password reset (public, token)
POST /auth/verify-email/confirm · POST /account/verify-email/request Email verification
GET /account/export · DELETE /account Data export & account deletion (self)
GET /providers/:id/verification A provider's trust badge (stages + dates)
POST /admin/providers/:id/verification Record a verification result (ops)
GET /consents Senior lists grants made; family lists grants held
DELETE /consents/:id Senior revokes a grant
POST /match Ranked candidates + care-plan options
POST /funding/plan Funding screen + plans at both out-of-pocket and funded budgets
POST /care-plans Propose a plan (body: needs profile, optional planKey)
GET /care-plans/:id Fetch a plan
POST /care-plans/:id/approve Senior approves (activates) a plan
POST /care-plans/:id/visits Schedule visits (body: { weeks, startDate? })
GET /care-plans/:id/visits List a plan's visits
GET /visits/:id Fetch a visit
POST /visits/:id/actions Apply a lifecycle action (body: { kind, … })
GET /seekers/:id/notifications Notifications generated for a senior

All rows below /health require Authorization: Bearer <token>. Visit action kinds: en_route, check_in (selfieMatchScore, distanceMeters), check_out, note (mealPrepared, medsReminded, mood), review (rating, voiceComment), cancel (reason), no_show.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages