Member Portal API
The Member Portal API is a stateless Backend-for-Frontend (BFF) for the Olly member web portal and mobile app. It owns no data and runs no migrations: every response is composed by calling one or more downstream services and handing the result back to the client. Owned by the member-portal-api service.
Field reference: the response shapes below are the BFF's own member-friendly projections, not catalog tables. The underlying domain columns live in the catalog: glossary terms
Claim,Party,Coverage,Accumulator,ConsentRecord,NotificationPreference. This page is the narrative.
Member scoping
Every /me/* route is scoped to the authenticated member; no party identifier appears in the path. memberScopeMiddleware reads a custom partyLocator JWT claim if present and otherwise falls back to the Keycloak sub claim, then injects that locator into the request context. Handlers pass it to downstream services, so a member only ever reads or writes their own data. A request that carries no claims, or no usable locator, gets a 401.
The BFF keeps the member-facing surface stable while the downstream topology changes underneath it. A breaking change to Eligibility, Claims or Consent is absorbed by editing the BFF, not the mobile client.
API routes
All routes sit behind JWT auth and member scoping. /healthz and /readyz are the only unauthenticated endpoints.
| Method | Path | Downstream | Description |
|---|---|---|---|
GET | /me/coverage | Eligibility | Member's active coverage summary |
GET | /me/accumulators | Eligibility | Deductible and out-of-pocket balances |
GET | /me/claims | Claims | List the member's claims |
POST | /me/claims | Claims | Submit a claim (returns 201 Created) |
GET | /me/claims/{locator} | Claims | Get one claim |
GET | /me/documents | Document Service | List the member's documents (EOBs, policy docs) |
GET | /me/documents/{locator} | Document Service | Get one document's metadata (type, generation date, and a downloadUrl pointing to the binary) |
GET | /me/profile | Policy Admin | Member's party details |
PATCH | /me/profile | Policy Admin | Update profile (first/last name, phone, email) |
GET | /me/preferences | Notifications | Notification channel preferences (email/push/sms) |
PUT | /me/preferences | Notifications | Replace notification preferences |
GET | /me/consent | Consent | Current consent state |
PUT | /me/consent | Consent | Replace consent preferences |
POST /me/claims requires a serviceDate in the body and returns 422 if it is missing. A malformed JSON body on any write route returns 400.
Documents are metadata, not bytes
GET /me/documents/{locator} returns JSON, not a PDF. The handler fetches a DocumentSummary from the Document Service and re-serialises it as application/json. The binary lives behind the downloadUrl field on that metadata; the BFF never streams document bytes (no Content-Disposition / octet-stream path exists in the service).
Dependencies
The BFF holds one HTTP client per downstream, each configured from a required *_URL environment variable. A missing or unreachable downstream surfaces as 502, never a partial response.
| Service | Routes served |
|---|---|
| Eligibility | GET /me/coverage, GET /me/accumulators |
| Claims | GET /me/claims, POST /me/claims, GET /me/claims/{locator} |
| Document Service | GET /me/documents, GET /me/documents/{locator} |
| Policy Admin | GET /me/profile, PATCH /me/profile |
| Notifications | GET /me/preferences, PUT /me/preferences |
| Consent | GET /me/consent, PUT /me/consent |
Events
The Member Portal API neither publishes nor consumes Kafka events. It is request/response only; any eventing happens in the downstream services it calls.
Invariants
- No database, no migrations, no local state. Any instance can serve any request, and the service scales independently of the data owners.
- Every
/me/*response is scoped to the caller'spartyLocator(custom claim, elsesub); a request with no resolvable identity is rejected with401. - Downstream errors map to a fixed contract:
ErrNotFound→404,ErrUpstream(5xx or unreachable) →502, anything else →500. The mobile client gets the same semantics regardless of which downstream failed.
Caveats
- It is a pass-through, not a cache. There is no read-through caching or fan-in aggregation across services in a single call; each route maps to one downstream call.
- Response shapes are the BFF's own. The structs returned (
CoverageResponse,AccumulatorsResponse,ClaimSummary,DocumentSummary, etc.) are trimmed member-facing projections, not the raw downstream payloads, so field-for-field they will not match the catalog domain tables.
