Access control designed around real clinic teams.
Six organization roles, scoped at the server boundary. Enforced through Appwrite Functions, not hidden in the UI.
This is the working specification for how access is granted, scoped, and revoked inside a MediFlow organization. It mirrors the model in the codebase rather than the marketing one.
The permission matrix.
Six roles, scoped to the work.
- 01Owner
- PHIfull
- Billingfull
- Settingsfull
- Membersfull
Manages organization, subscription, seats, users, sensitive settings, and ownership transfer.
- 02Admin
- PHIfull
- Billingread
- Settingsfull
- Membersnon-owner
Manages workspace users, records, and operations without owner-level authority over other owners.
- 03Clinician
- PHIfull
- Billingnone
- Settingsnone
- Membersnone
Owns clinical work: records, vitals, documents, appointments, and enabled AI workflows.
- 04Staff
- PHIscoped
- Billingnone
- Settingsnone
- Membersnone
Supports day-to-day operations: patient viewing, scheduling, limited data entry.
- 05Billing
- PHIread
- Billingfull
- Settingsnone
- Membersnone
Accesses billing information and payment workflows without broad clinical write access.
- 06Readonly
- PHIread
- Billingnone
- Settingsnone
- Membersnone
Supervisory, audit, handover, or expired-subscription access. No mutations.
Rules the system will not break.
- 01The last owner cannot be removed or demoted.
- 02Admins cannot manage owner memberships.
- 03Billing states can block non-billing mutations.
- 04Expired or suspended workspaces become read-only for non-billing writes.
- 05Appwrite Team memberships are the source of truth for membership and role.
- 06Seat limits block invites when the plan limit is reached.
Server-authoritative, not UI-hidden.
Permission decisions belong at the server boundary. Client-side hide/show is an experience improvement; it is never the source of truth for PHI, billing, membership, or administrative operations.
import { session, assertRole, requireOrgScope } from "@/lib/auth";
import { audit } from "@/lib/audit";
export async function handler({ req, log }) {
const { user, team } = await session(req);
await assertRole(team, ["owner", "admin", "clinician"]);
await requireOrgScope(team, req.body.patientId);
await audit.write({
actor: user,
action: "patient.update",
class: "phi.write",
targetId: req.body.patientId,
});
// PHI mutation runs only after role + scope + audit succeed.
return updatePatient(req.body);
}Billing state can block writes.
Pending or past-due states block non-billing mutations. Expired and suspended states move the workspace into read-only behavior while owner and billing recovery paths stay open.
{
"event": "audit.decision",
"ts": "2026-05-24T10:14:02Z",
"actor": { "type": "user", "role": "clinician" },
"org": { "id": "org_8f3", "plan": "growth" },
"billingState": "past_due",
"decision": "block",
"class": "phi.write",
"rule": "billing.state.gate",
"metadata": { "targetType": "patient", "phi": false }
}Evidence trail.
- coming soonRBAC test suite output
- coming soonSample audit export
- coming soonPermission change log
- previewReadiness status board