Skip to content
Updated Jun 9, 2026

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 PostGIS location point kept in sync from lat/lng by a DB trigger (migration 0010).
  • 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

TablePurpose
providersProvider 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_reviewsMember reviews: provider_id FK, author, rating (1 to 5), comment; aggregates are computed on read, not stored
credentialing_requestsCredentialing 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.

MethodPathAuthDescription
POST/providersJWTCreate a provider (npi and name required; partyLocator optional)
GET/providersJWTList providers (filters: specialty, networkStatus)
GET/providers/listJWTList alias for web-admin
GET/providers/nearestJWTNearest providers by lat/lng (optional specialty, limit)
GET/providers/{locator}JWTGet provider by locator (includes aggregate rating/reviewCount)
PATCH/providers/{locator}JWTUpdate provider details
GET/providers/{locator}/reviewsJWTList reviews plus aggregate rating
POST/providers/{locator}/reviewsJWTAdd a member review (author, rating 1-5)
PATCH/providers/{locator}/activateJWTSet provider ACTIVE
PATCH/providers/{locator}/deactivateJWTSet provider INACTIVE
POST/credentialingJWTSubmit a credentialing request (providerLocator required; provider must exist)
GET/credentialingJWTList credentialing requests (filter: providerId)
GET/credentialing/listJWTList alias for web-admin
GET/credentialing/{locator}JWTGet credentialing request by locator
PATCH/credentialing/{locator}/approveJWTApprove (only from SUBMITTED; sets provider ACTIVE)
PATCH/credentialing/{locator}/rejectJWTReject (only from SUBMITTED)
GET/credentialing/{npi}/statusJWTLatest credentialing status for an NPI
GET/internal/providers/{npi}InternalFetch 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.

eventTypeEmitted whenState subjects
provider.createdPOST /providers commits a new PENDING providerprovider
provider.searchedGET /providers (or /providers/list) runs a directory searchnone (behavioural event, no row of its own)
provider.activatedPATCH /providers/{locator}/activate sets ACTIVEprovider
provider.deactivatedPATCH /providers/{locator}/deactivate sets INACTIVEprovider
provider.reviewedPOST /providers/{locator}/reviews records a member reviewreview, provider
credentialing.submittedPOST /credentialing files an application (SUBMITTED)credentialing, provider
credentialing.approvedPATCH /credentialing/{locator}/approve (also activates the provider)credentialing (best-effort: absent if the post-decision re-read fails)
credentialing.rejectedPATCH /credentialing/{locator}/rejectcredentialing (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

ServiceHow used
Policy AdminOn 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 becomes ACTIVE only via approved credentialing or a direct activate, and activated_at is stamped on activation.
  • Approve and reject act only on a SUBMITTED request; any other current status returns 409 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 PostGIS location point) are nullable; providers without coordinates never appear in nearest results. address jsonb stays free-form and is not geocoded.
  • Statuses are TEXT with a default, not DB CHECK enums. network_status defaults to PENDING and status to SUBMITTED; 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_seq are created in the public schema, unlike providers / credentialing_requests which live in the provider schema.

Olly Health Insurance Platform