Skip to content

Repository files navigation

BDMS — Buprenorphine Dispensing Management System

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.


The problem

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 approach

The system replaces the per-centre paper register with one shared record.

  1. A doctor registers a patient once. The system assigns a permanent, network-wide patient ID (BDMS-000001).
  2. 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.
  3. 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.
  4. 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.
  5. 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.


Roles

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.

Tech stack

  • 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

Getting started

Prerequisites

1. Clone and install

git clone <your-fork-url> bdms
cd bdms
npm install

2. Set up the database

In 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.

3. Configure environment variables

cp .env.example .env.local

Fill 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.

4. Create the first admin

Because there is no sign-up flow, the first admin has to be created by hand. In the Supabase dashboard:

  1. Authentication → Users → Add user. Set an email and password, and tick Auto Confirm User.
  2. On that user, set the User Metadata to { "role": "admin" }. The proxy reads this to route you to the right dashboard.
  3. 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.

5. Run it

npm run dev

Open 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 .next

Project structure

src/
├── 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

Data model

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 remaining
  • get_dispensing_timeseries(start, end, granularity, center, drug) — approved vs denied over time
  • get_dispensing_breakdown(start, end, center, drug) — denial reasons and per-centre activity
  • get_user_role() — used inside the RLS policies

Limitations & known gaps

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_eligibility and then inserts the log. A modified client could insert an approved log for an ineligible patient. Production would need the insert wrapped in a SECURITY DEFINER function 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 the profiles table, 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.log debugging is still present throughout.
  • Centre names are placeholders seeded in schema.sql. Replace them.

Disclaimer

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.

About

Buprenorphine Distribution Management System - prototype

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages