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:
- Role-based access control (RBAC) to show each pilot group a dashboard card that other users don't see.
- Relationship-based access control (ReBAC) to let members share their health data with caregivers, and to let the members of a group read each other's profiles.
- Attribute-based access control (ABAC) to limit caregiver access to a date range.
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.
| Feature | What it does in GHC |
|---|---|
| Member data ownership | Each member holds the owner resource role on their own member instance, which derives owner on their profile, health plan, and medical records. |
| Member groups | A 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 access | A 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 delegation | A 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 groups | The 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
memberandprofilein 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:
| Member | Starting state | |
|---|---|---|
| Rick Sanchez | rick@example.com | Admin of the Ricks Org and Smith Family groups, in the Benefits pilot group |
| Morty Smith | morty@example.com | Member of Smith Family, and caregiver of Rick's member record for a year from setup |
| Bird Person | birdperson@example.com | Member of Ricks Org, in the Alternative Medicine pilot group |
| Homer Simpson | homer@example.com | In 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
| Entity | Definition |
|---|---|
| Resource | A type of object you manage access to. Each resource can have many instances, such as one member's health plan. |
| Action | An operation a user can perform on a resource. The policy allows or denies each action. |
| Relation | A named link between two resource types. GHC uses parent (from member) and belongs (from member_group). |
| Relationship tuple | One instance of a relation between two resource instances, such as member_group:member_group_rick_smith_family belongs profile:profile_morty. |
| Resource role | A role that applies to instances of one resource type. Permit writes it as Resource#Role, for example health_plan#caregiver. |
| Role derivation | A 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 role | A 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) | Actions | Resource roles | Relations | Role derivations |
|---|---|---|---|---|
Member (member) | read, write | owner (read, write), caregiver (read) | Subject of parent on profile, health_plan, and medical_records | None. Both roles are assigned directly on a member instance. |
Profile (profile) | read, write | owner (read, write), caregiver (read) | parent from member, belongs from member_group | owner from member#owner via parent. caregiver from member#caregiver via parent and from member_group#org_member via belongs. |
Health Plan (health_plan) | read | owner (read), caregiver (read) | parent from member | owner from member#owner and caregiver from member#caregiver, both via parent. |
Medical Records (medical_records) | read | owner (read), caregiver (read) | parent from member | owner from member#owner and caregiver from member#caregiver, both via parent. |
Member Group (member_group) | list, assign | admin (list, assign), org_member (list) | Subject of belongs on profile | None. Both roles are assigned directly on a member_group instance. |
Benefits (benefits_pg) | view | None | None | None. The top-level Benefits Pilot Group role (benefits_pg_role) grants benefits_pg:view. |
Alternative Medicine (alt_medicine_pg) | view | None | None | None. 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.
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.
| Endpoint | What it returns | Check | Resource roles that allow it |
|---|---|---|---|
GET /api/account/profile/{user} | One user's profile details | read on profile:profile_{user} | profile#owner, profile#caregiver |
GET /api/account/dashboard/health-plan/{user} | One user's health plan | read on health_plan:health_plan_{user} | health_plan#owner, health_plan#caregiver |
GET /api/account/dashboard/medical-records/{user} | One user's medical records | read 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 delegation | write on member:member_{signed-in user} | member#owner |
POST /api/account/member-groups/assign, POST /api/account/member-groups/unassign, DELETE /api/account/member-groups | Add or remove a group member, or delete a group | assign on member_group:{group} | member_group#admin |
POST /api/permissions | One decision for each action and resource in the request body, for the signed-in member | A bulk check of what the page asked about | Whichever role grants each action. permit-fe-sdk calls this route. |
GET /api/account/member | The users in the groups the signed-in user administers | None. The route lists admin and org_member role assignments through the Permit API. | Not applicable |
GET /api/account/member-groups, POST /api/account/member-groups | List the signed-in user's groups, or create a group with the user as its admin | None | Not applicable |
GET /api/allowed-plans | The users whose dashboards the signed-in user can open | None. 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.
- Sign in as
rick@example.com. Rick's dashboard shows only Rick's own data, because each user holdsmember#owneron their own member instance. - 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. - Sign in as Rick and open Delegate Permissions. The wizard lists the members of Rick's groups: Morty and Bird Person.
- Share Rick's medical records with Bird Person, and set a date range that includes today. The app assigns Bird Person the
caregiverrole onmedical_records:medical_records_rick. - Sign in as
birdperson@example.comand open Rick's dashboard from the plan selector.
Verify: each member sees the following.
| Member | What the member sees | Why |
|---|---|---|
| Rick | His own profile, health information, and health plan, and the Personal Benefits card | Rick holds member#owner, which derives owner on his profile, health plan, and medical records. The Benefits Pilot Group role grants benefits_pg:view. |
| Morty | Rick's profile, health information, and health plan | Morty holds member#caregiver on member:member_rick, which derives caregiver on Rick's profile, health plan, and medical records. |
| Bird Person | Rick's profile and health information, but not Rick's health plan | Bird 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."}. |
| Homer | Only his own data, with no plan selector | Homer 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:
| Option | How it works | When to use it |
|---|---|---|
| Condition set in the Permit dashboard | A 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 policy | You 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:
-
Replace the content of
custom/root.regowith the content ofpolicy/custom.regofrom the example folder. -
Edit the top-level
root.regofile. By default,root.regohas twoallowrules, one forpolicies.allow(the generated policy) and one forcustom.allow, so access is allowed if either rule allows it. Remove the secondallowrule, and put both conditions in oneallowrule so that both must pass:allow {policies.allowcustom.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.
- In the Delegate Permissions wizard, share Rick's medical records with Bird Person and set a range that includes today.
- Sign in as Bird Person and open Rick's dashboard.
- 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.
- 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 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
- Read the galactic-health-corporation source to see every check in context.
- Build ReBAC policies with resource roles and role derivations.
- Write custom policies with GitOps to add Rego rules alongside the generated policy.
- Teleco Demo Application for a policy model built with the Node.js SDK.
- Check permissions with permit.check() for the check options the app uses.