A prototype for tracking and regulating the dispensing of Buprenorphine across a network of government de-addiction centres, so that a patient cannot collect the same prescription more than once within its prescribed interval — at their own centre or at any of the other 21.
This is a prototype / proof of concept. It is not certified medical software, has not been clinically or security audited, and must not be used to handle real patient data. See Limitations before you consider doing anything serious with it.
Buprenorphine is used at de-addiction centres for the detoxification and maintenance treatment of heroin addiction. Because it is itself an opioid, it carries its own misuse potential — and misuse defeats the very purpose of the treatment.
Patients collect their doses on a daily or weekly basis. Where collection records are kept on paper and locally to each centre, a patient can present at one centre on Monday and a second centre on Tuesday, and neither has any way to know. Across 22 centres this creates a real and invisible leak.
The goal: every tablet accounted for, and no duplicate collection anywhere in the network.
The system replaces the per-centre paper register with one shared record.
- A doctor registers a patient once. The system assigns a permanent,
network-wide patient ID (
BDMS-000001). - The doctor issues a prescription with a dosage and a cooldown period — the minimum number of hours that must pass between collections (24h for daily, 168h for weekly). A patient can have only one active prescription at a time; this is enforced by a database constraint, not by application code.
- When a patient presents at any centre, the pharmacist enters their ID. The database runs an eligibility check that looks at the last approved collection across all centres, not just the current one.
- Both approvals and denials are written to an append-only dispensing log. A denied attempt is as valuable as an approved one — it is the evidence that someone tried to double-collect.
- The admin sees the whole network: dispensing volume over time, denial reasons broken down by cause, and per-centre activity.
The cooldown check is the core of it, and it lives in the database
(check_patient_eligibility) rather than in the UI, so every client sees the
same answer:
patient exists? → patient active? → active prescription?
→ hours since last approved collection
(any centre) ≥ cooldown_hours?
If any check fails, the pharmacist gets a specific reason — including exactly how many hours remain and which centre served the patient last.
| Role | Can do |
|---|---|
| Admin | Create and edit staff accounts, assign pharmacists to centres, view all patients, view network-wide analytics |
| Doctor | Register patients, issue and revise prescriptions, set cooldown periods, view their own patients |
| Pharmacist | Look up a patient by ID, run the eligibility check, dispense or log a denial — scoped to the centre they are assigned to |
There is no public sign-up. Every account is created by an admin.
- Next.js 16 (App Router, React Server Components) + React 19
- TypeScript
- Supabase — Postgres, Auth, Row Level Security, and Postgres functions (RPC) for the business logic
- Tailwind CSS v4 with shadcn-style primitives (Radix UI,
cva) - Recharts for the admin analytics
- Motion for the landing page animations
- Node.js 20+
- A free Supabase project
git clone <your-fork-url> bdms
cd bdms
npm installIn the Supabase dashboard, open the SQL Editor and run the entire contents
of supabase/schema.sql. It is idempotent — it drops and
recreates everything, so it is safe to re-run while you are experimenting (it
will also wipe your data).
This creates the centers, profiles, patients, prescriptions and
dispensing_logs tables, the eligibility and analytics functions, the RLS
policies, and seeds 22 placeholder centres.
cp .env.example .env.localFill in the three values from Project Settings → API in Supabase:
| Variable | Where it's used |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Browser + server |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Browser + server (RLS applies) |
SUPABASE_SERVICE_ROLE_KEY |
Server only — the admin API routes. Bypasses RLS. Never expose it to the client. |
Because there is no sign-up flow, the first admin has to be created by hand. In the Supabase dashboard:
- Authentication → Users → Add user. Set an email and password, and tick Auto Confirm User.
- On that user, set the User Metadata to
{ "role": "admin" }. The proxy reads this to route you to the right dashboard. - Copy the new user's UUID, then in the SQL Editor:
INSERT INTO profiles (id, full_name, email, role)
VALUES ('<paste-the-uuid>', 'Your Name', 'you@example.com', 'admin');Every account after this one can be created from Admin → Staff → Add staff.
npm run devOpen http://localhost:3000, sign in at /login, and you will land on the
dashboard for your role.
npm run build # production build
npm run start # serve the production build
npm run lint # eslint
npm run clean # remove .nextsrc/
├── app/
│ ├── page.tsx landing page
│ ├── (auth)/login/ sign-in, routes by role
│ ├── (protected)/
│ │ ├── layout.tsx server-side auth gate
│ │ ├── admin/ dashboard, staff CRUD, all patients
│ │ ├── doctor/ dashboard, patient registry, prescriptions
│ │ └── pharmacist/ dashboard, dispense flow
│ └── api/admin/
│ ├── create-user/ service-role: create auth user + profile
│ └── update-user/ service-role: update auth user + profile
├── components/
│ ├── admin-analytics.tsx Recharts dashboards
│ ├── forms/ dispense flow, prescriptions, patients, staff
│ └── ui/ button, dropdown, theme toggle, effects
├── proxy.ts Next.js 16 proxy (formerly middleware):
│ session refresh + role-based routing
├── types/database.ts hand-written Supabase types
└── utils/supabase/ browser / server / proxy clients
supabase/schema.sql full database schema, functions, RLS, seed
.agents/ Supabase agent skills used while building
| Table | Purpose |
|---|---|
centers |
The 22 de-addiction centres |
profiles |
One row per auth user: role, assigned centre, active flag |
patients |
Network-wide patient registry, auto-numbered BDMS-000001 |
prescriptions |
Drug, dosage, cooldown hours. Partial unique index enforces one active prescription per patient |
dispensing_logs |
Append-only audit trail of every approved and denied attempt |
Database functions:
check_patient_eligibility(patient_id)— the cooldown / prescription / status check, returns JSON with a reason and hours remainingget_dispensing_timeseries(start, end, granularity, center, drug)— approved vs denied over timeget_dispensing_breakdown(start, end, center, drug)— denial reasons and per-centre activityget_user_role()— used inside the RLS policies
Being explicit about what this prototype does not do, since the subject matter invites the assumption that it does:
- Eligibility is enforced in the UI, not at write time. The pharmacist
client calls
check_patient_eligibilityand then inserts the log. A modified client could insert anapprovedlog for an ineligible patient. Production would need the insert wrapped in aSECURITY DEFINERfunction that re-runs the check atomically. - No protection against a simultaneous double-collection. Two centres
checking the same patient at the same instant will both see "eligible". A row
lock or a unique constraint on
(patient_id, cooldown window)is needed. - Identity is a typed-in patient number. Nothing stops someone reading out a number that is not theirs. Real deployment needs biometrics, a scanned card, or a photo on file.
- RLS is coarse. Any authenticated user can read all patients, prescriptions and dispensing logs. Centre-level and role-level read scoping is not implemented.
- The role used for routing lives in
user_metadata, which an authenticated user can write to. Page-level checks re-read the role from theprofilestable, so this is a routing convenience rather than the security boundary — but it should not stay that way. - No stock or inventory tracking. The log records dispensing events, not tablet counts, so "every tablet accounted for" is only half-solved: you can reconcile collections, not physical stock.
- No prescription history view, no patient-facing anything, no offline mode,
no printed receipts, no tests. Verbose
console.logdebugging is still present throughout. - Centre names are placeholders seeded in
schema.sql. Replace them.
Built as a prototype to explore the problem. Not affiliated with, endorsed by, or deployed at any health department or de-addiction programme. No real patient data is present in this repository. Do not use it for clinical decision-making.