Skip to main content

Healthcare Demo Application

Run the Galactic Health Corporation (GHC) demo app and see how one healthcare app combines three permission models in Permit.io. This worked example is for developers who design authorization for apps with delegated access, such as patients who share records with caregivers.

GHC is a Next.js sample app set in the universe of "Rick and Morty". The app uses:

note

The code is in the galactic-health-corporation folder of the permitio/examples repository. For a longer walkthrough, read Building healthcare authorization with Next.js on the Permit blog.

Galactic Health Corporation features​

GHC app key features​

Every feature below maps to a resource, action, or role that scripts/policy.ts creates in your Permit environment.

FeatureWhat it does in GHC
Member data ownershipEach member holds the owner resource role on their own member instance, which derives owner on their profile, health plan, and medical records.
Member groupsA group administrator holds member_group#admin and assigns other users the member_group#org_member role. The members of a group read the profiles in that group.
Delegated caregiver accessA member grants another user the caregiver resource role on their whole member record, their health plan, or their medical records. The caregiver then reads that data.
Time-limited delegationA caregiver_bounds user attribute holds a start and end date per delegation. An optional custom Rego policy denies the caregiver role outside that range.
Pilot groupsThe Benefits Pilot Group role grants view on the benefits_pg resource, and the Alternative Medicine Pilot Group role grants view on alt_medicine_pg. Each role shows one card on the member's own dashboard.

Run GHC locally​

Prerequisites​

  • Docker with Compose, to run the app, its setup, and the policy decision point (PDP) container. See Install Docker Engine.
  • A Permit.io environment made for this example, and its environment API key. Setup creates resources with plain keys such as member and profile in the environment. See Get your API key.

Start the app​

The galactic-health-corporation README has the steps. In short, you set PERMIT_API_KEY in .env and run docker compose up --build. Compose starts a PDP container, a one-shot setup service that writes the policy and four demo members to your Permit environment, and the app on http://localhost:3000. The app signs members in with Better Auth (email and password), and keeps its accounts in a SQLite database in a Compose volume.

All demo members sign in with the password galactic-health:

MemberEmailStarting state
Rick Sanchezrick@example.comAdmin of the Ricks Org and Smith Family groups, in the Benefits pilot group
Morty Smithmorty@example.comMember of Smith Family, and caregiver of Rick's member record for a year from setup
Bird Personbirdperson@example.comMember of Ricks Org, in the Alternative Medicine pilot group
Homer Simpsonhomer@example.comIn no group, with no caregivers

A member who signs up on the sign-up page starts like Homer: the owner of their own data and nothing else. When someone signs up, a Better Auth hook registers them in Permit with the owner role on their member instance and the three parent relationship tuples.

Verify: In the Permit dashboard, open the Policy screen and the Resources tab. Seven resources appear: member_group, member, profile, health_plan, medical_records, benefits_pg, and alt_medicine_pg. Then sign in to the app as rick@example.com. The dashboard shows Rick's profile, health information, and health plan, and the Personal Benefits card.

RBAC and ReBAC model​

GHC builds most of its policy with ReBAC, and uses two top-level RBAC roles for the pilot groups.

Entities​

EntityDefinition
ResourceA type of object you manage access to. Each resource can have many instances, such as one member's health plan.
ActionAn operation a user can perform on a resource. The policy allows or denies each action.
RelationA named link between two resource types. GHC uses parent (from member) and belongs (from member_group).
Relationship tupleOne instance of a relation between two resource instances, such as member_group:member_group_rick_smith_family belongs profile:profile_morty.
Resource roleA role that applies to instances of one resource type. Permit writes it as Resource#Role, for example health_plan#caregiver.
Role derivationA rule that grants a resource role to users who hold another resource role on a related instance. In GHC, roles on a member instance derive roles on that member's profile, health plan, and medical records.
Top-level roleA role assigned in a tenant rather than on an instance. GHC has two: Benefits Pilot Group and Alternative Medicine Pilot Group.

ReBAC requirements​

  • Users read their own profile, health plan, and medical records.
  • Users read the profiles in the member groups they belong to.
  • Users delegate read access to their member record, health plan, or medical records to another user, for a specific date range.
  • Users in a pilot group see that group's card.

scripts/policy.ts creates this model. Resource and role keys in the table are the keys the app checks against:

Resource (key)ActionsResource rolesRelationsRole derivations
Member (member)read, writeowner (read, write), caregiver (read)Subject of parent on profile, health_plan, and medical_recordsNone. Both roles are assigned directly on a member instance.
Profile (profile)read, writeowner (read, write), caregiver (read)parent from member, belongs from member_groupowner from member#owner via parent. caregiver from member#caregiver via parent and from member_group#org_member via belongs.
Health Plan (health_plan)readowner (read), caregiver (read)parent from memberowner from member#owner and caregiver from member#caregiver, both via parent.
Medical Records (medical_records)readowner (read), caregiver (read)parent from memberowner from member#owner and caregiver from member#caregiver, both via parent.
Member Group (member_group)list, assignadmin (list, assign), org_member (list)Subject of belongs on profileNone. Both roles are assigned directly on a member_group instance.
Benefits (benefits_pg)viewNoneNoneNone. The top-level Benefits Pilot Group role (benefits_pg_role) grants benefits_pg:view.
Alternative Medicine (alt_medicine_pg)viewNoneNoneNone. The top-level Alternative Medicine Pilot Group role (alt_medicine_pg_role) grants alt_medicine_pg:view.

Instance keys follow the pattern <resource>_<user key>. The health plan of the user rick is the instance health_plan:health_plan_rick. A group's profiles are the profiles of its admin and its members: each one has a belongs tuple with the group.

The profile caregiver role has no date bounds

profile#caregiver is derived from member_group#org_member as well as from member#caregiver, so every user in a member group can read the profiles in that group. The custom Rego policy in ABAC model for time-limited delegation leaves out profile roles for that reason. Date bounds apply to member records, health plans, and medical records.

API endpoints​

GHC's Next.js API routes run the following checks. Every route answers HTTP 401 without a session, HTTP 403 with {"error":"Not allowed."} when Permit denies the request, and HTTP 503 when the PDP is unreachable. An endpoint with no check filters its results with the Permit API instead of calling permit.check(). A {user} segment is optional and defaults to the signed-in member.

EndpointWhat it returnsCheckResource roles that allow it
GET /api/account/profile/{user}One user's profile detailsread on profile:profile_{user}profile#owner, profile#caregiver
GET /api/account/dashboard/health-plan/{user}One user's health planread on health_plan:health_plan_{user}health_plan#owner, health_plan#caregiver
GET /api/account/dashboard/medical-records/{user}One user's medical recordsread on medical_records:medical_records_{user}medical_records#owner, medical_records#caregiver
GET, POST /api/account/caregiver, DELETE /api/account/caregiver/{caregiver}/{resource}The signed-in user's caregivers, and grant or revoke a delegationwrite on member:member_{signed-in user}member#owner
POST /api/account/member-groups/assign, POST /api/account/member-groups/unassign, DELETE /api/account/member-groupsAdd or remove a group member, or delete a groupassign on member_group:{group}member_group#admin
POST /api/permissionsOne decision for each action and resource in the request body, for the signed-in memberA bulk check of what the page asked aboutWhichever role grants each action. permit-fe-sdk calls this route.
GET /api/account/memberThe users in the groups the signed-in user administersNone. The route lists admin and org_member role assignments through the Permit API.Not applicable
GET /api/account/member-groups, POST /api/account/member-groupsList the signed-in user's groups, or create a group with the user as its adminNoneNot applicable
GET /api/allowed-plansThe users whose dashboards the signed-in user can openNone. The route lists the signed-in user's caregiver role assignments through the Permit API.Not applicable

In the browser, src/components/PermissionsProvider.tsx uses permit-fe-sdk to send one bulk check to POST /api/permissions, and turns the answers into a CASL ability. The dashboard shows each pilot-group card inside <Can I="view" a="benefits_pg"> and <Can I="view" a="alt_medicine_pg">. See Integrate CASL with Permit.

Test the caregiver flow​

Test the ReBAC model with the demo members.

  1. Sign in as rick@example.com. Rick's dashboard shows only Rick's own data, because each user holds member#owner on their own member instance.
  2. Sign in as morty@example.com. The plan selector next to the title lists Rick, because setup made Morty a caregiver of Rick's member record. Open Rick's dashboard.
  3. Sign in as Rick and open Delegate Permissions. The wizard lists the members of Rick's groups: Morty and Bird Person.
  4. Share Rick's medical records with Bird Person, and set a date range that includes today. The app assigns Bird Person the caregiver role on medical_records:medical_records_rick.
  5. Sign in as birdperson@example.com and open Rick's dashboard from the plan selector.

Verify: each member sees the following.

MemberWhat the member seesWhy
RickHis own profile, health information, and health plan, and the Personal Benefits cardRick holds member#owner, which derives owner on his profile, health plan, and medical records. The Benefits Pilot Group role grants benefits_pg:view.
MortyRick's profile, health information, and health planMorty holds member#caregiver on member:member_rick, which derives caregiver on Rick's profile, health plan, and medical records.
Bird PersonRick's profile and health information, but not Rick's health planBird Person's member_group#org_member role on Ricks Org derives profile#caregiver on Rick's profile, and Rick shared the medical records directly. No derivation reaches Rick's health plan, so GET /api/account/dashboard/health-plan/rick returns 403 with {"error":"Not allowed."}.
HomerOnly his own data, with no plan selectorHomer holds no role on another member's data.

ABAC model for time-limited delegation​

ABAC requirements​

A delegation must be limited to a date range. For example, Rick gives Bird Person access to his medical records for one week only.

Date boundaries on role assignments​

A caregiver's caregiver resource role comes from a role assignment on another member's data. A resource role assignment has no expiry of its own, so GHC bounds it with a user attribute and a custom Rego rule.

GHC stores the bounds in a caregiver_bounds user attribute on the caregiver. Each key in caregiver_bounds is the resource instance the caregiver was granted access to, and each value holds an RFC 3339 start_date and end_date. The POST /api/account/caregiver handler writes this attribute with permit.api.users.update() before it assigns the caregiver role. For Morty's grant on Rick's member record, the user has these attributes:

{
"key": "morty",
"attributes": {
"caregiver_bounds": {
"member:member_rick": {
"start_date": "2026-11-10T12:00:00.000Z",
"end_date": "2027-11-10T12:00:00.000Z"
}
}
}
}

The custom Rego policy in policy/custom.rego finds the caregiver role that allowed a ReBAC decision, either assigned on the instance or derived from the member record, and allows the decision only while the current time falls inside that instance's bounds:

within_bounds(resource) {
bounds := abac.attributes.user.caregiver_bounds[resource]
time.now_ns() >= time.parse_rfc3339_ns(bounds.start_date)
time.now_ns() <= time.parse_rfc3339_ns(bounds.end_date)
}

The policy allows every decision that no caregiver role took part in. time.parse_rfc3339_ns rejects any value that is not an RFC 3339 timestamp, so write the dates in the format shown above. The caregiver route stores them with Date.prototype.toISOString().

Implementation options​

You can bound a role by time in Permit in two ways:

OptionHow it worksWhen to use it
Condition set in the Permit dashboardA user set matches the user when an expiry attribute is still ahead of a current_time attribute that your app passes in each check. See Time Based Role Example.You bound a top-level role, and you can add the attributes through the dashboard.
Custom Rego policyYou write Rego that Permit combines with the generated policy, and the PDP evaluates both. See Write custom policies with GitOps.You bound a derived resource role, which condition sets don't reach.

GHC uses the custom Rego option, because the caregiver role is derived through a relationship rather than assigned directly. The policy is policy/custom.rego in the example folder, and policy/custom_test.rego tests it with OPA. Without the custom policy, the app still stores and shows the dates, but Permit doesn't enforce them.

Connect Permit to a policy repository with GitOps​

Custom Rego lives in the policy repository, so connect Permit to a Git repository first. Permit writes the generated policy to that repository, and you change the custom policy through branches and pull requests. See Connect a GitHub repository to Permit for the setup steps.

Add the custom policy​

In the branch of your policy repository that Permit syncs for your environment:

  1. Replace the content of custom/root.rego with the content of policy/custom.rego from the example folder.

  2. Edit the top-level root.rego file. By default, root.rego has two allow rules, one for policies.allow (the generated policy) and one for custom.allow, so access is allowed if either rule allows it. Remove the second allow rule, and put both conditions in one allow rule so that both must pass:

    allow {
    policies.allow
    custom.allow
    }

For more on custom Rego in Permit, see Write custom policies with GitOps.

Test the time-limited delegation​

Run the caregiver flow from Test the caregiver flow again, with two date ranges.

  1. In the Delegate Permissions wizard, share Rick's medical records with Bird Person and set a range that includes today.
  2. Sign in as Bird Person and open Rick's dashboard.
  3. Sign back in as Rick, revoke the delegation on the Control Access page, and share the medical records again with a range that ends before today.
  4. Sign in as Bird Person and open Rick's dashboard again.

Verify: with the range that includes today, Bird Person sees Rick's health information. With the range that ends before today, GET /api/account/dashboard/medical-records/rick returns 403 with {"error":"Not allowed."} and the dashboard shows no health information. The generated policy still grants medical_records#caregiver, and custom.allow denies the request because the current time is outside the bounds.

The PDP evaluates custom Rego only after GitOps syncs it

The custom rule runs only once Permit has synced the policy repository branch for your environment and the PDP has pulled it. Until then, the PDP evaluates the generated policy alone, and a delegation whose date range has passed still grants access. Check the sync status in Settings, then GitOps, before you test.

Next steps​