A medication and health tracker built with PHP and MySQL. It works in mobile and desktop browsers.
HealthPHP · CSSGitHub @MichelleFindlayOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
A medication and health tracker built with PHP and MySQL. It works in mobile and desktop browsers.
How to use it: see the documentation, also in the app under Docs in the footer (and the ? next to headings).Read the full README ↗A medication and health tracker built with PHP and MySQL. It works in mobile and desktop browsers.
How to use it: see the documentation, also in the app under Docs in the footer (and the ? next to headings).
config/config.php). Medicines can be marked as a private prescription or bought
without one, with their price: these are logged when a new supply is recorded and kept out of the PPC sums.The calendar icon in the top bar opens Appointments. Add the date, time, length, type (GP, specialist, hospital, dentist, optician, blood test, scan, physio, mental health, vaccination, ...), who it's with, where, notes and an optional reminder. The next two weeks also show on the Today page, and the doctor's report lists appointments.
Add to your calendar creates a private calendar link (calendar.php?token=…) unique to each person.
Subscribe to it in Apple Calendar, Google Calendar or Outlook, and new or changed appointments appear there
automatically. Calendar apps check every few hours.
'public_url' in the app section of
config/config.php). Until one is set, Pillbox uses the address an administrator last opened it at, never just
the address a request claims (which anyone can fake). If links show the wrong address, set it.Tap an appointment to open its page. It has:
Print / save as PDF gives a one-page sheet with tick boxes and space to write answers.
Meds → ✈ Travel lets you plan a trip: where you're going (its time zone), when you leave and when you get back, and how dose times should change:
| Option | What happens (e.g. 08:00 dose, London → Tokyo) |
|---|---|
| Switch to local time | 08:00 Tokyo time. Home time resumes the day after you get back. |
| Stay on home time | 16:00 Tokyo time, the same moment as 08:00 in London. |
| Shift gradually | Moves 1–4 hours a day until it's 08:00 Tokyo time, then moves back the same way after you return. |
While you're away, all times in Pillbox are shown in local time, and each dose also shows its home time. If any medicines are time-critical (insulin, contraceptive pills, epilepsy, Parkinson's or HIV medicines), check with a doctor or pharmacist before travelling.
Apple doesn't let websites read Apple Health directly, so Readings → Import from Apple Health offers two ways:
export.zip (from the phone itself or a computer). Large exports are uploaded in pieces, so your web host's
upload limit doesn't matter. Re-importing never creates duplicates.What's imported:
Units are converted to Pillbox's (kg, mmol/L, °C, %). Steps and sleep recorded by both an iPhone and a Watch are only counted once. Measurements that receive data are switched on automatically. Imported readings are labelled "Apple Health", and Remove all imported readings takes them out again without touching readings you typed in yourself.
The server needs PHP's xmlreader extension (standard) and, to read export.zip directly, zip. Without
zip, unzip the export and upload export.xml instead.
mod_rewrite (normally enabled) and ideally mod_headerspdo_mysql, curl, openssl, mbstring; zip, xmlreader and zlib for imports,
policy documents and self-updates (all standard)Upload the whole folder, for example to /var/www/pillbox or into your web space.
Create a database and user, either in phpMyAdmin or with:
CREATE DATABASE pillbox CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pillbox'@'localhost' IDENTIFIED BY 'a-strong-password';
GRANT ALL PRIVILEGES ON pillbox.* TO 'pillbox'@'localhost';
You don't need to import any tables, because Pillbox creates them the first time it is opened.
You can import database/schema.sql yourself if you prefer.
Configure: copy config/config.example.php to config/config.php and fill in the
database details. config/config.php is the only settings file; it is git-ignored, so your passwords stay off GitHub. If the site is in a sub-folder (e.g. https://example.com/pillbox/), set
'base_path' => '/pillbox'. If config/config.php is empty or damaged (for example overwritten by an upload),
Pillbox shows a page saying so rather than an error.
Make storage/ writable by the web server: chown www-data storage (or set it to 775 in your file
manager). It holds sessions, the encryption key for private pages and sign-in tokens (app.key), the
notification keys, the setup code, update backups, encrypted photos and caches. Back it up, especially
app.key: without it, private pages can't be read.
Open the site. On a new install you'll be asked to create the first administrator account, with a one-time
setup code so nobody else who finds the site first can take it over. The code is in storage/setup_code.txt
on the server, and in PHP's error log (docker logs, or your host's error log); or set your own as 'setup_code'
under 'app' in config.php. It's deleted once the administrator exists. After 10 wrong codes an address is
paused for 15 minutes, and after 40 wrong codes from all addresses together everyone is (it just keeps being
told the code isn't right; the error log says so). If you're
stuck, clear it with DELETE FROM login_attempts WHERE username = 'setup';. Behind a reverse proxy or load
balancer, set 'trust_proxy' => true so each visitor's own address is used. Everyone's
first sign-in then goes through a short setup (about you, what to use, reminders), and administrators get a
Site settings step too: site name and address, default time zone, email, scheduled tasks, demo mode.
Those are saved in (git-ignored), which overrides , so config.php
itself, with its comments and database details, is never rewritten; they can be changed later on the Admin
page. If the folder isn't writable, Pillbox shows what to paste into config.php instead.
Best: point the virtual host's DocumentRoot at the public/ folder:
<VirtualHost *:443>
ServerName pillbox.example.com
DocumentRoot /var/www/pillbox/public
<Directory /var/www/pillbox/public>
AllowOverride All
Require all granted
</Directory>
# ... SSL settings ...
</VirtualHost>
Shared hosting / can't change it: upload the whole folder into the web root (or a sub-folder).
The top-level .htaccess sends requests into public/ and blocks src/, config/, database/ and
storage/. Each of those folders also has its own deny-all .htaccess as a fallback. This needs
AllowOverride All (or at least FileInfo and AuthConfig).
Check it: browsing to https://your-site/config/config.php should return 403 Forbidden.
Pillbox is a Progressive Web App (PWA). Once it's on your HTTPS site, people can add it to their home screen. It then opens full screen with its own icon, like a normal app; no App Store or Google Play listing is needed. Account → Install Pillbox on your phone explains how:
Notes:
session_idle_minutes (8 hours by default) since last use, even if the app is
closed. Raise it in config/config.php if people find they're signed out too often, e.g. 10080
for a week.storage/sessions, so storage/ must be writable.https://your-site/auth_callback.php (or .../pillbox/auth_callback.php).email claim.Pillbox.Admin. Then in
Enterprise applications → Pillbox → Users and groups, assign it to people who should manage users.
Admin rights are then synced from Entra at each sign-in. Set 'admin_role' => '' to manage admins
only inside Pillbox.config/config.php, set the entra section:
'entra' => [
'enabled' => true,
'tenant_id' => '<Directory (tenant) ID>',
'client_id' => '<Application (client) ID>',
'client_secret' => '<secret value>',
'redirect_uri' => 'https://your-site/auth_callback.php',
],
How accounts are created:
auto_provision on (the default), anyone from your tenant gets a Pillbox account on first sign-in.Multi-tenant: set tenant_id to organizations and list the permitted tenant GUIDs in allowed_tenants. Only list tenants you control or trust. Pre-registered accounts are only linked by email for people from your tenant_id or a tenant listed by GUID; never through '*' (any tenant), since another tenant could claim someone else's email address.
Sign-ins from any other tenant are rejected.
You can run local and Microsoft accounts side by side. To allow Microsoft accounts only, set
'allow_local_login' => false.
Besides Microsoft, Pillbox can sign people in with any OpenID Connect provider, such as Google, Okta,
Auth0, Keycloak, Authentik, Authelia or Zitadel. Add as many as you like under 'oidc' => ['providers' => [...]]
in config/config.php; each gets its own button on the login page. The template has a Google example plus
commented examples for Okta, Keycloak and Authentik.
For each provider:
https://your-site/oidc_callback.php (the same URL for every provider).config/config.php, set issuer to the provider's issuer URL and
'enabled' => true.| Provider | issuer |
|---|---|
https://accounts.google.com | |
| Okta | https://YOUR-ORG.okta.com (or https://YOUR-ORG.okta.com/oauth2/default) |
| Auth0 | https://YOUR-TENANT.auth0.com/ |
| Keycloak | https://HOST/realms/REALM |
| Authentik | https://HOST/application/o/SLUG/ |
Options per provider:
auto_provision: create an account on first sign-in. With Google, always combine this with
allowed_email_domains, or anyone with a Google account can sign up.allowed_email_domains: only accept verified email addresses at these domains, e.g. ['example.com'].admin_claim / admin_values: make people admins from a claim in the ID token, e.g.
'groups' / ['pillbox-admins']. This only applies to accounts that sign in only with that provider.single_logout: also sign out of the provider when signing out of Pillbox.How accounts work:
auto_provision off, an admin adds them under Admin → Add a user
and picks the provider. They're matched by their verified email the first time they sign in.Pillbox uses the authorisation-code flow with PKCE. It checks every ID token's RS256/384/512 signature against the provider's published keys, plus the issuer, audience, expiry and nonce.
'disable_local_login_when_sso' => true (in the app section) turns off password sign-in whenever
Microsoft or any OIDC provider is enabled.
storage/app.key).remember_days (90 by default; 0 turns it off) after
it was last used. Only a hash of its token is stored. Signing out, changing the password and Sign out everywhere
else forget remembered devices.'geoip_lookup' => true (in the app section; off by default) it also shows an approximate city and
country, by sending the IP address to ipapi.co at sign-in. The privacy policy then says so.Everything clinical follows a person's body and medicines, never a gender field:
storage/private/), with its own PIN, never shared, and
deleted in one go.Terms and Privacy pages are linked in every page's footer, and people agree to them when they sign up (the
date is saved with the account). The text is generated from what Pillbox actually does: what it stores, encryption,
sharing, the optional services you've switched on (email, push, connected apps, dm+d, single sign-on, weather,
IP location), its cookies, and people's rights under UK GDPR. Fill in who runs it, a contact and the governing law under Admin → Site
settings → Terms and privacy (or 'legal' in config/config.php), or replace either text with your own. It's a
starting point, not legal advice.
By default only admins add people. To let people create their own accounts, turn on Admin → Site settings →
Sign-ups (or set it in config/config.php):
'signup' => [
'enabled' => true, // "Create an account" on the sign-in page
'require_approval' => true, // new accounts wait for an admin to approve them
'notify_admins' => true, // email admins about each new sign-up
],
require_approval, a new account can't be used until an admin presses Approve under
Waiting for approval on the Admin page (admins also see a notice on Today). Decline removes the
account. People who gave an email address are emailed when they're approved.notify_admins and the mail settings filled in, every active admin with an email
address is told about each sign-up, with a link to the Admin page.'auto_provision' => true (a trusted Microsoft tenant or company directory) still create
accounts on first sign-in whatever these settings say, without approval. With open sign-ups and a public
provider such as Google, anyone with an account there can sign up, so keep require_approval on or
set allowed_email_domains.When adding a medication, you can search the NHS Dictionary of Medicines and Devices (dm+d) and pick the exact product, e.g. "Metformin 500mg modified-release tablets". Pillbox fills in the name, strength, form and dose unit, and stores the product's dm+d code, which is shown on the doctor's report. Typing details in by hand still works.
The search uses the NHS England Terminology Server. dm+d is licensed content, so you need a free system-to-system account:
config/config.php:
'nhs_terminology' => [
'enabled' => true,
'client_id' => '<client id>',
'client_secret' => '<client secret>',
],
Only the search words are sent to the NHS server, never anything about the patient.
As of October 2026, NHS England doesn't let new apps read a patient's medication record:
If that changes, the stored dm+d codes are the same codes GP records use, so imported medicines could be matched to ones already in Pillbox.
The installed version is in the VERSION file and is shown in the footer, with a link to the GitHub
repository (github_repo in config/config.php; set it to '' to hide both). For admins, Pillbox compares
its VERSION with the newest published release on GitHub, at most twice a day, and shows
Update available in the footer when that release is newer (or Development version when this copy is
ahead of it). Drafts and pre-releases are ignored. The Admin page's Version section has Check now, the
Changelog (every release's notes) and the Licenses of Pillbox and the code it includes.
VERSION (e.g. 1.0.0 → 1.1.0), merge it into main,
then publish a GitHub release tagged with the same number (v1.1.0 or 1.1.0).github_token. Without it, GitHub hides the repository and the check reports
that it couldn't look.Fill in the 'mail' settings in config/config.php: 'transport' => 'mail' uses PHP's mail() (most shared
hosting), or 'smtp' with a host, port, 'secure' => 'tls' (587) or 'ssl' (465), username and password.
Each person chooses under Account → Email:
Admins are emailed once when a new version is released. The scheduled tasks also refresh the update check and tidy up (old sign-in attempts, leftover upload files, stale sessions, finished demo accounts). They check for updates but never install them. Send me a test email on the Account page checks the settings.
Under Sharing (the people icon), you can give someone with an account on the same Pillbox access to parts of yours. For each part (medicines and doses, readings, appointments, Care, Mind, reports) you choose no access, see, or a level of change: for medicines, log doses (take, skip, collected prescriptions) or change medicines. You can keep chosen Care categories private (e.g. Periods and contraception), include or leave out the Mind journal, and set an end date. Quick choices: "See everything except Mind", "Help with medicines", "Full help (not Mind)".
Each part's permission applies wherever its information appears, not just on its own page: someone without Readings doesn't see readings in Care, on Today or in the report, and someone without Medicines doesn't see medicines anywhere (including the headache diary). Private medicines and the private pages are never shared.
They open it from their own Sharing page, with a banner showing whose Pillbox it is and Back to mine; only the tabs and parts they've been given appear. Pages are checked centrally, and anything not shared is refused. They can never see or change your account settings, sign-in details, sync and calendar links, data export or Mind PIN, can't permanently delete medicines, and can't share your Pillbox further. Every change they make is listed under their name (Recent activity), and you can stop sharing at any time.
Sharing permissions have a regression test. Run it against a test database (it works inside a transaction that is rolled back, but don't point it at live data):
php tests/sharing_medicines_test.php
It checks the Migraine page for three viewers: the owner, someone with Medicines shared, and someone without. Only the first two see medicine names, and private medicines are never shown to anyone but the owner.
Set 'demo' => ['enabled' => true] in config/config.php to add Try the demo to the sign-in page.
Each visitor gets their own copy of a sample account for Nell Whitcombe, a made-up person (the same one as
the ICE demo), with a few weeks of doses, readings, appointments, Care and Mind data. Everything they change
is deleted when they end the demo or sign out, or after 'hours' if they just leave. Sign-in details,
email, sharing, Apple Health sync, calendar links and passkeys are switched off in the demo, and
demo accounts don't appear in the admin's user list.
Readings → Connect apps and devices brings readings in from other places, labelled with where they came from and never duplicated:
.../connect.php), and enters its
client ID and secret under Admin → Site settings → Connected services. Each person then presses
Connect. Readings sync about once an hour via the scheduled tasks; sign-in tokens are stored encrypted
(key in storage/app.key). They bring in resting heart rate, heart rate variability, oxygen level, sleep,
breathing rate, steps, weight, and (Withings) blood pressure and temperature, depending on the service.Each person can turn on notifications in each browser or device they use (Account → Notifications):
dose reminders at dose time (if not logged), Care reminders and a daily "coming up", appointment reminders,
and the Mind check-in. Hide details on the lock screen shows just "Pillbox: you have a reminder". They use
the standard Web Push protocol (VAPID keys are created automatically in storage/; messages are encrypted
for the device), so no third-party service or account is needed. They're sent by the scheduled tasks, so run
those every 5 minutes. On iPhone and iPad, notifications work once Pillbox has been added to the Home Screen
(iOS 16.4 or later). Notifications are only sent to the browsers' own push services (Google, Mozilla, Apple,
Microsoft); to allow another, such as a self-hosted UnifiedPush server, list its host in
'push' => ['allowed_hosts' => ['push.example.com']] (a leading dot allows its subdomains too).
Admins can update from the Admin page: Install version X now downloads the release from GitHub, checks
it, saves a backup of the current files in storage/backups/, then replaces them. config/config.php and
storage/ are never touched, and the database updates itself on the next page load. Any backup can be put
back with Restore. It needs PHP's zip and curl extensions and write access to the Pillbox folder; copies
that are git checkouts should use git pull. Uploading the files by hand works too.
With Apache's mod_rewrite, pages have addresses without ".php" (e.g. /medications); public/.htaccess
turns this on and tells Pillbox, which then makes its links that way and redirects old ".php" addresses.
Calendar feed and Apple Health sync addresses keep working as they are. Without mod_rewrite (or with
'pretty_urls' => false), everything uses ".php" as before.
Account → Your data → Download all my data gives one file with everything (readable sections, plus a complete backup). Import data restores that backup into an account, replacing what's there: useful for moving to another Pillbox. Sign-in details, sharing, passkeys and sync links aren't included.
password_hash. Local logins are rate-limited per username and per IP, and
optional two-factor sign-in covers every way of signing in (see Signing in safely).public/ web root: pages, assets (CSS, JS, logo, icons), service worker
src/ application code (auth, single sign-on, two-factor, scheduling, metrics, charts, layout, Care
categories, Mind, sharing, mail, push, tasks, updater, demo)
bin/ cron.php: the scheduled tasks, for the command line
config/ config.example.php (template) -> copy to config.php (your settings); settings.php (Site settings)
database/ schema.sql
docs/ the user documentation, also shown in the app (Docs in the footer)
tests/ regression tests (run against a test database)
vendor/ bundled third-party code (qrcode-generator, MIT)
storage/ keys, sessions, backups, encrypted photos and caches (must be writable; back it up)
Pillbox is free software, released under the GNU General Public License v3.0. You can use, study, change and share it. If you share it (changed or not), you must share the source code under the same licence.
Copyright (C) 2026 Michelle Findlay
Pillbox is a record-keeping aid. It is not medical advice; always follow your prescriber's instructions.
'weather' => falseapp'nightscout' => ['allow_private' => true].storage/app.key) with an optional
PIN. It's never shared, and never appears on Today, in the calendar, emails, reports, backups or data downloads.
Reminders only say "You have a reminder", and Delete everything removes it completely.config/settings.phpconfig/config.phpconfig/Set up the scheduled tasks (optional, but needed for emails, notifications and connected apps): run them
every 5 minutes (notifications are sent on time only then; up to 15 is fine without them), either
with cron, */5 * * * * php /var/www/pillbox/bin/cron.php, or, if your host has no cron, set a long
random token under 'cron' in config/config.php and have a web cron service call
https://your-site/cron.php?token=.... The Admin page shows when they last ran and what they did.