Provider
A Provider is a healthcare entity (physician, facility, lab) that can be looked up by Olly and, once credentialed, marked in-network. The provider service owns the directory, the credentialing workflow, and member reviews. There are three entities: Provider, CredentialingRequest and ProviderReview. Credentialing is the gate that flips a provider from PENDING to ACTIVE.
Field reference: full columns, types and nullability live in the catalog: glossary terms
Provider,CredentialingRequest. This page is the narrative.
What the service owns
- The provider directory: create, read, update, activate, deactivate; network status per provider.
- The credentialing application: submit, approve, reject, and status lookup by NPI.
- Member reviews on a provider profile (#1108): per-provider review list plus an aggregate rating/count computed on read.
- Nearest-provider search:
GET /providers/nearest?lat=&lng=over an optional PostGISlocationpoint kept in sync fromlat/lngby a DB trigger (migration0010). - An internal NPI lookup (
GET /internal/providers/{npi}, no JWT) for Claims and Care to resolve an NPI to a provider record.
List filtering is by specialty and networkStatus; spatial lookup is the separate nearest route and only matches providers with coordinates set.
Provider network status
NetworkStatus = PENDING · ACTIVE · INACTIVE. A newly created provider defaults to PENDING. It reaches ACTIVE either by an approved credentialing request or by a direct activate call; deactivate sets INACTIVE. There is no OUT_OF_NETWORK value; out-of-network is the absence of ACTIVE.
Credentialing workflow
CredentialingStatus = SUBMITTED · APPROVED · REJECTED (the authoritative enum in packages/go/domain/enums.go). A request is created SUBMITTED (the DB default), and approve/reject are only allowed from SUBMITTED; a request already approved or rejected returns 409 Conflict.
Approval is the only path that mutates the provider: on approve, the service sets the credentialing request to APPROVED and, in the same operation, sets the linked provider to ACTIVE and stamps activated_at. Rejection records the decision and notes but leaves the provider untouched. Reviewer notes from the request body are stored as reviewer_notes; reviewed_at is stamped on either decision.
Credentialing is a separate entity rather than a boolean on the provider, which keeps an auditable record of each application and decision.
Database
Schema: provider
| Table | Purpose |
|---|---|
providers | Provider entities: NPI, specialty, contact info, address jsonb, network_status (default PENDING), optional party_id/party_locator, optional lat/lng plus a trigger-synced PostGIS location point (GIST-indexed) |
provider_reviews | Member reviews: provider_id FK, author, rating (1 to 5), comment; aggregates are computed on read, not stored |
credentialing_requests | Credentialing applications: provider_id FK, status (default SUBMITTED), reviewed_at, reviewer_notes |
Locators are minted from two sequence tables, provider_locator_seq and credentialing_locator_seq. Both live in the public (default) schema, not the provider schema. NextLocator does UPDATE ... SET val = val + 1 RETURNING val, producing PRV-{year}-{seq} and CRD-{year}-{seq}.
API Routes
JWT-protected unless noted. The /.../list rows are aliases of the bare list routes that web-admin reaches through APISIX.
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /providers | JWT | Create a provider (npi and name required; partyLocator optional) |
GET | /providers | JWT | List providers (filters: specialty, networkStatus) |
GET | /providers/list | JWT | List alias for web-admin |
GET | /providers/nearest | JWT | Nearest providers by lat/lng (optional specialty, limit) |
GET | /providers/{locator} | JWT | Get provider by locator (includes aggregate rating/reviewCount) |
PATCH | /providers/{locator} | JWT | Update provider details |
GET | /providers/{locator}/reviews | JWT | List reviews plus aggregate rating |
POST | /providers/{locator}/reviews | JWT | Add a member review (author, rating 1-5) |
PATCH | /providers/{locator}/activate | JWT | Set provider ACTIVE |
PATCH | /providers/{locator}/deactivate | JWT | Set provider INACTIVE |
POST | /credentialing | JWT | Submit a credentialing request (providerLocator required; provider must exist) |
GET | /credentialing | JWT | List credentialing requests (filter: providerId) |
GET | /credentialing/list | JWT | List alias for web-admin |
GET | /credentialing/{locator} | JWT | Get credentialing request by locator |
PATCH | /credentialing/{locator}/approve | JWT | Approve (only from SUBMITTED; sets provider ACTIVE) |
PATCH | /credentialing/{locator}/reject | JWT | Reject (only from SUBMITTED) |
GET | /credentialing/{npi}/status | JWT | Latest credentialing status for an NPI |
GET | /internal/providers/{npi} | Internal | Fetch provider by NPI (no JWT) |
Events
The service produces only, publishing directly to Kafka on topic provider.events (pre-created; broker auto-create is off). There is no outbox: provider.searched changes no row of its own, and the row-changing events are published fire-and-forget after the write, so a failed publish never fails the HTTP request (and is not retried).
Each message is the canonical platform envelope (see the event catalog): eventId, eventType, occurredAt, client lineage (sessionId / activityId / activityName, lifted from W3C baggage on the request context), payload, and state (event-carried state: the subject entities frozen at emit time, as named subjects such as {"provider": ...}). The direct producer stamps no correlationId; trace context rides in the Kafka message headers instead.
eventType | Emitted when | State subjects |
|---|---|---|
provider.created | POST /providers commits a new PENDING provider | provider |
provider.searched | GET /providers (or /providers/list) runs a directory search | none (behavioural event, no row of its own) |
provider.activated | PATCH /providers/{locator}/activate sets ACTIVE | provider |
provider.deactivated | PATCH /providers/{locator}/deactivate sets INACTIVE | provider |
provider.reviewed | POST /providers/{locator}/reviews records a member review | review, provider |
credentialing.submitted | POST /credentialing files an application (SUBMITTED) | credentialing, provider |
credentialing.approved | PATCH /credentialing/{locator}/approve (also activates the provider) | credentialing (best-effort: absent if the post-decision re-read fails) |
credentialing.rejected | PATCH /credentialing/{locator}/reject | credentialing (best-effort: absent if the post-decision re-read fails) |
Credentialing approval activates the provider in the same service call but announces itself only as credentialing.approved; provider.activated fires solely on the direct activate route.
Events consumed: none. Payload schemas, lineage/state contracts and golden examples live in the event registry (packages/go/domain/eventregistry/registry/<eventType>/).
Dependencies
| Service | How used |
|---|---|
| Policy Admin | On provider create with a partyLocator, resolves the party via GET /internal/parties/{locator} to link party_id/party_locator. Not called during credentialing. |
The Policy Admin client exposes only GetParty. It confirms a party exists and returns its id/locator; it does not read network tiers or plan participation, and those concepts are not modelled in this service.
Invariants
- A provider starts
PENDING; it becomesACTIVEonly via approved credentialing or a directactivate, andactivated_atis stamped on activation. - Approve and reject act only on a
SUBMITTEDrequest; any other current status returns409 Conflict. - Approving a credentialing request also activates its provider, in the same service call; rejecting does not change the provider.
- NPI is unique across providers and is the internal lookup key for Claims and Care; locators (
PRV-/CRD-) are unique and monotonic per the sequence tables.
Caveats
- Coordinates are optional.
lat/lng(and the trigger-maintained PostGISlocationpoint) are nullable; providers without coordinates never appear innearestresults.address jsonbstays free-form and is not geocoded. - Statuses are TEXT with a default, not DB CHECK enums.
network_statusdefaults toPENDINGandstatustoSUBMITTED; the enum set is enforced in Go, not by Postgres. - NPI as the lookup key.
GET /internal/providers/{npi}resolves by National Provider Identifier (not internal locator) because upstream callers receive NPIs from external sources. - Sequence tables are not schema-qualified.
provider_locator_seq/credentialing_locator_seqare created in the public schema, unlikeproviders/credentialing_requestswhich live in theproviderschema.
