OpenMasjid Students
Tuition & fee management for your madrasa — pay online, at the kiosk, or on the donation site.
A self-hosted tuition/fee app that runs as an OpenMasjidOS app — one Docker container, all data on the masjid's own hardware.
OpenMasjid Students is the madrasa's tuition desk. The office keeps its students and households, assigns fee plans per child, and generates one invoice per child each month or term. The office or finance manager records cash / Zelle / check payments against the child they were paid for and sees the whole ledger; parents get a phone-first portal showing what each child owes, one combined household balance, and one payment history — and they can pay by card in the app (Stripe), with saved cards and autopay.
Every student gets an auto-generated Student ID (YUS1234), unique per install. It is how a
parent pays without an account: type the ID on the masjid's OpenMasjid Donations site or
OpenMasjid Kiosk, confirm the name it shows back, and pay for any of their children from that
one screen. Those payments land in the same ledger over the OpenMasjidOS Fabric — this app
provides the students/billing capability those apps consume
(see docs/FABRIC_BILLING_CONTRACT.md).
The academic layer is coming back, and the first half has landed (0.52.0 →). This app was deliberately narrowed to fees and money in v0.35.0, and this notice used to say academics were gone for good. That decision is reversed, in phases. Shipped in v0.52.0: a real student record (admission and withdrawal dates, prior schooling, languages and nationality on the household, medical fields behind an admin-only wall, office notes) and a full admissions funnel (a public inquiry form you can embed in your own site, a waitlist, admission and re-admission forms that reach a family as a one-time link, and a waiting-room tablet mode). Still specified and not yet built: the daily register, the gradebook and report cards. See The academic layer for what each phase covers, and
CLAUDE.md§4a for the per-phase status, which is the authority.What is not coming back: a timetable, a teacher or student login, user-defined custom fields, and file uploads of any kind.
The complete feature set, by area.
- One name field per student — many madrasa names do not split into two western halves, so
the app stores one authoritative
fullNameand derives everything it needs from it. - Auto-generated Student ID at registration (first three letters of the name + 4 digits, e.g.
YUS1234), unique per install, retrievable by design because it is printed on statements. Never chosen, never imported, no PINs. - Optional date of birth, with age shown where it helps.
- Status — active or withdrawn; a withdrawn child stops appearing on payment paths.
- A real student record (0.52.0) — date of admission, date of withdrawal and the reason, previous school and hifz on arrival, plus medical notes, allergies and emergency medical consent. Every one of these can be switched off in Settings, and the medical three are admin-only and off until you turn them on: the finance manager never sees them, they never reach a parent screen, an alert, a log or an export. The household — not the child — keeps the address, languages spoken and nationality, so a family of three has one copy rather than three that drift.
- Office notes with an author and a timestamp. They cannot be edited, so a correction is another note and nobody's record of a conversation is quietly rewritten; an admin can delete one, audited, for the note typed onto the wrong child.
- Directory with search, and a student picker used wherever a child must be chosen.
- Backfill for Student IDs, so an install that predates them fills every gap once and is a no-op forever after.
- Deletion is pre-checked, not attempted-and-refused. The app tells you exactly what a child is attached to before anything happens, and money-referencing records make an ordinary Delete refuse.
- An admin — never finance — can still erase a billed child for good, behind a second dialog that names the invoices, the payments and the money it will destroy and asks for the child's name to be typed. For the install that bills a test roster by accident and was otherwise stuck with children it could only withdraw. The audit row is written first, and it does not touch Stripe.
- Households form by linking siblings — nobody is ever asked to name a family. Add a child, or link an existing one to a sibling, and the two households merge; unlinking splits them again.
- The household label is derived from the children's surnames ("Ismail family", or "Farooqi / Ismail" when they differ), sorted so it never depends on who was added first.
- Sibling suggestions surface likely matches instead of making you hunt, and a whole sibling group can be linked at once.
- Guardians attach to the household, not the child — which is exactly why linking a sibling is what makes the parents' details apply to them. Nothing is copied per student.
- One guardian can span several households (blended families), and each household can carry several guardians. Link, unlink, update, and remove — with a removability pre-check.
- Emergency contacts per household: flag a guardian, or add extra contacts.
- Spreadsheet import — the office's own .xlsx or CSV, with the columns mapped on screen rather than forced into a template, guardians and siblings merged from continuation rows, dates read in the format you configured, and a preview that names every row it would refuse before anything is written. Committing is a separate step. (The .xlsx is read without a spreadsheet library — it is a ZIP of XML and the browser has both halves.)
- Schools, for a masjid running more than one program on different calendars — a weekend maktab beside a full-time hifz school. A school scopes the calendar and the class tree, and never the household or the money: a family with a child in each is still one household, one balance, one portal login and one printed sheet. A staff account can optionally be limited to some schools, which narrows what they see and never widens what they may do.
- School years with a start and end month, so a year running Apr → Mar wraps into the next calendar year correctly. Set the current year, archive an old one, or delete an unused one.
- A real year rollover — promote classes and carry children forward into the new year, rather than a flag flip that leaves last year's roster in place.
- Optional terms within a year, so a
per-termfee cadence means something concrete. - Courses → classes as a two-level grouping, with archive, delete and a deletability check.
- Assign a student to a class, individually or in bulk, and list students by class.
- Organizational only, today: no teachers, no attendance, no grades, no capacity. These labels drive the roster, the year view, and applying fees to many children at once. The academic layer (below) builds on these same labels rather than beside them — a class stays one thing.
- A public inquiry form families fill in themselves — at
/public/inquiry, or embedded in the masjid's own website with a one-line snippet. It renders into your page and inherits your site's own fonts and form styling rather than sitting in a box of our design. It is the only part of the app reachable without signing in, and it is rate-limited, honeypotted, capped per day and restricted to the origins you list. - A pipeline the office actually works — new, waitlisted, moved to admission, declined, admitted. An inquiry is not a student: it mints no Student ID and never appears in the directory.
- An admission form that reaches the family as a one-time link, pre-filled from what they already told you on the inquiry, asking whatever fields you have switched on. It can also stand open on a waiting-room tablet — local to the masjid network by default, with an opt-in if you need otherwise, and carrying a revocable device token rather than anybody's login.
- Nothing a family submits is written until an admin approves it. It lands as a proposal the office reviews, striking out any single answer first.
- Re-admission, folded into starting a new school year: ask the families you already have whether they are returning, pre-filled, and approve a diff instead of a blind overwrite.
- Admission and re-admission fees are raised as ordinary charges through the same machinery that bills everything else — re-approving cannot charge a family twice.
- With more than one school, an unrestricted admin routes an inquiry to the school that will handle it; that school's own staff see it from then on, and never an unrouted one.
- Fee plans — a name, an amount in whole cents, and a cadence:
monthly,per-term, orone-time. Archive a plan that is no longer offered; delete one nothing has used yet. - Assigned per student (never per class enrollment), so each child's bill is their own.
- Per-child amount override — which is also how a bursary, hardship rate or sibling discount is expressed, since with one bill per child a household-level discount has nowhere honest to sit.
- Bulk assign a plan across a class or a whole year, and unassign just as easily.
- Fee plans are admin-only to define; finance can see them and assign them.
- A reusable catalog of charge items — a book fee, a trip, an exam fee — created once and applied when needed, rather than retyped each time. Update, archive, or delete an unused one.
- Add a charge to one child or to many at once, list what has been charged, and void one that was a mistake.
- Charges appear as their own lines on the invoice, so a parent can see and pay them separately from tuition.
- A charge is billed on its own, straight away, by default (0.51.0). Its own one-line invoice, due today, payable immediately — because waiting for the month's run meant generating every child's tuition early or telling the parent to come back. Putting it on a period is still there as a choice. A credit is the exception: it has to reduce something, so it goes on a period.
- One invoice per child per period — the household still sees one combined balance and pays once, but the money always lands on the right child's record.
- Generate for a whole period (a month or a term) or for a single household on demand.
- Automatic nightly generation on a day of the month you choose, with a configurable due day. The job decides for itself whether today is the day, whether the month falls inside the current school year, and whether it has already run for this period — so a missed night is caught up rather than skipped, and a period is never billed twice. "Run now" is there too.
- Statuses — open, partly paid, paid, void — all derived from what has actually been paid.
- Void an invoice raised in error — and bill that month again afterwards (0.51.0), which used to be impossible: the voided row held the slot for good, so the month could never be re-billed and the screen just said "Generated 0". Any charge that was on the voided bill comes back as owed.
- What a year comes to, per child, on their billing record — the question every enrollment opens with. Counted from where that child's billing actually starts to the end of the year, at their own agreed amount, over the months this madrasah really teaches. It is a quote and writes nothing.
- A child joining mid-year can have their first month set to whatever was agreed — it appears as its own named line under the full tuition, rather than a rewritten amount nobody can account for.
- Per-term fee plans are not billed yet. The cadence can be configured and assigned, but invoices are generated by month, so nothing raises a per-term line. Both screens say so, and the year quote leaves it out rather than promising money that will not be collected.
- Itemized lines with one canonical order (tuition → charges → credits), used by both the display and the allocator, so a balance never appears against the wrong line.
- Year view — a grid of every child against every month of the year, showing at a glance who has been billed and who has paid, with configurable columns.
- Mid-year go-live. A madrasa adopting this in February records what each child brings with them exactly once: a past-dated "balance carried forward" invoice if they owe, or a dated credit if they are ahead. You approve a preview computed by the same code that then writes it, the months before go-live are never generated, and the app afterwards refuses to generate them — because recording the autumn as one figure and generating September would bill the same arrears twice. Clearable if you get it wrong.
- Every balance is derived (
invoiced − paid), never stored, so it cannot drift out of step. - Payments are immutable. A correction is a reversal row, never an edit or a delete.
- A payment belongs to one child and auto-allocates to their invoices, oldest due first. Overpayment becomes that child's credit and comes off their next invoice automatically.
- A household balance is simply the sum of its children's, because one adult pays for all of them — and one card charge covering several children is recorded as one row per child, keyed so that a replay is a no-op per child.
- Allocation is per line, and a payer's instruction survives. When a parent chose lines — "this $50 is the book fee" — that choice is stored on the payment and re-honored every time the mapping is recomputed. Without that, the next invoice would silently undo it and the line they deliberately settled would read as outstanding again.
- Re-allocation is a recompute, not an increment, so money paid before a bill existed attaches itself the moment that bill appears.
- Idempotent everywhere — the same payment reference can arrive twice from any channel and the second one changes nothing.
- Recording a payment is the first thing on the billing screen: type a few letters of a name or paste a Student ID, and the amount, method and date are right there. It doubles as a dropdown for browsing the roster when you are not sure how a name is spelled.
- Manual channels — cash, Zelle, check, or other — with the date it actually arrived and a memo.
- Choose which lines a payment settles, or let it allocate oldest-due-first.
- "Recorded by" names the staff member on every manual payment. Cash is the one payment nobody else can verify — no card, no Stripe record, nothing to reconcile against — so the office can answer "who took this?". Card payments say "Automatic". It shows on reversals and in the CSV too.
- A family who always pays the same way can be recorded automatically (0.51.0). Set the channel — cash, check, bank transfer, Zelle — and a day of the month on the child's billing record, and the app writes the payment for you. It records it whether or not the money arrived, which the screen says plainly; two things stop that going wrong quietly — it records only what is actually OWED that day, so it can never mint credit, and it can never record the same month twice.
- Reverse a payment — a new row, fully audited, never a rewrite of history.
- Refund a transaction, grouped the way the parent actually paid. A card payment is refunded at Stripe and reversed on the ledger, in that order; cash is reversed on the ledger with the screen saying plainly that a person still has to hand the money over. Full refunds only — a partial goes back as a credit on the next bill — and a carried-forward balance is refused outright.
- Past due, chased on a policy you set — a grace period, a minimum worth chasing, and a cadence. A reminder goes to the parent naming which of their children is behind and for how much, and a digest goes to the office listing the students and their amounts. Reminders never restart a cooldown for a family nobody could actually reach.
- Every channel goes through one ledger function: cash, Zelle, check, other, the parent portal, autopay, the donation site, the kiosk, and a mid-year carry-in. The office sees the channel and the Stripe reference without doing anything to get them.
-
You can ask the payer to cover the card fee (0.51.0). Every card payment costs the madrasah about 2.9% + 30¢, so a $100 bill has always brought in $96.80. Switch this on and the payer is charged $103.30 instead. Off until you turn it on, never applied to cash or checks, with separate rates and separate switches for card and bank — and it is disclosed in three places, because each is a different moment: itemized at pay-now, worked on the household's balance on the autopay tab, and with a worked example on the printed family sheet. The fee rides on the payment and never enters the ledger.
-
Pay by card in the parent portal via Stripe Elements — the full balance, a chosen amount, or specific lines. Paying ahead is allowed even with nothing due (a term or a year up front), floored at a $1 minimum, and the surplus shows as credit.
-
Card details never touch this app's server. Elements handles them in the browser; the server only ever sees Stripe ids. Saved cards store brand, last four and expiry — never a card number.
-
Saved cards — add, remove, and put them in order: first choice, second, third. That order is the ladder autopay walks down on a decline, so a retry tries the next card rather than presenting the same declining one three times.
-
Autopay per household: switch it on and the default card is charged when tuition comes due. Clear consent copy, and the consent timestamp is stored.
-
Decline handling — a retry ladder rather than one attempt, an email to the parent on each failure with a "pay now / update card" link, and after the third failure autopay switches itself off, tells the parent, and alerts the office. A disabled enrollment is never charged.
-
A charge that never resolves is chased. A run left pending — a lost confirmation, a browser closed mid-payment — is resolved against Stripe rather than being silently re-charged or dropped.
-
Autopay is its own tab, with an offer on the portal's front page, and once it is on the same card tells the parent which card will be charged.
-
Daily reconciliation against Stripe (plus a "Reconcile now" button) records any succeeded tuition payment that never reached the ledger — a browser closed at the wrong moment, a dropped call from the kiosk — with the date the money actually arrived, flagged as recovered and alerted to the office. Money is never lost, only delayed.
-
Finance and admin see autopay status and which card will be charged — but only the parent can switch it off. An office asked to stop a charge has to talk to the household; the consent is theirs and so is the card.
- My family — every child with their Student ID and their own balance, each open itemized bill, and one unified payment history whatever channel it came from.
- A child with nothing due says so, rather than vanishing from a page that only lists debts.
- Bills read as a statement until you choose to pay part of one — tick boxes appear only after "choose what to pay", each bill is its own card with its lines indented beneath it, and the running total sits on the pay button. Everything starts ticked, so paying a whole bill is one tap.
- Lines already paid say so; a bursary shows as the deduction it is — both for information, neither payable.
- Credit is shown instead of a bare zero, so "paid ahead" never looks like "square".
- The year at a glance — the same grid of months the office sees, for their own children.
- Turn WhatsApp messages off for anyone on the household, and back on again.
- Receipts by email, worded as a payment rather than a donation — tuition is generally not tax-deductible.
- Own profile basics, and a password change that signs out your other devices.
- This app provides the
students/billingcapability that OpenMasjid Donations and OpenMasjid Kiosk consume, surfaced there as a tuition campaign: the tile lives in those apps, everything inside it is served from here. - The flow is Student ID → confirm the name shown back → see the balance → pay, for any of that household's children from one screen. The name-confirmation step is what replaced the PIN.
- Itemized bills reach those apps too, so a parent at the kiosk can pay one line.
- Pay-ahead works there as well, with the same minimum as the portal.
- The office can switch external payments off entirely, and the campaign disappears.
- Everything crosses through the platform's broker — this app never talks to those apps directly.
- Four printable documents, all HTML with a print stylesheet — no PDF renderer and no headless
Chromium, so they stay Pi-friendly and print from the browser dialog an office already knows, and
all of them read legibly in black and white on a masjid photocopier:
- the household statement — balance, open bills, recent payments, each child's Student ID, a QR code to the portal sign-up, and a line telling parents they can pay with a Student ID at the kiosk or on the donation site;
- the household information sheet, whose wording the office can rewrite in Settings;
- the per-child invoice;
- the Student ID sheet by class, for handing out at the start of a year. All carry the school's logo and color, and all are served locked down, since they contain Student IDs and payment history.
- CSV export of billing data, with spreadsheet formula-injection escaping.
- An append-only audit trail on every sensitive write — fee assignment, invoices, payments,
reversals, autopay changes, role and account changes, settings, structure — recording who, when,
and before → after.
recorded_byis the person for money; the audit actor is the account.
- First-run setup creates the first admin, on the masjid network by definition.
- Staff accounts — create, change role, disable, and reset a password. A forced password set on first login.
- Two doors for parents, both landing on the same guardian link:
- Invite — staff pick a guardian and the app emails a one-time link (or prints it, if there is no mail); the parent sets their own password. Resendable.
- Self sign-up — a child's Student ID plus a guardian email already on file for that child, then an emailed verification. The on-file address is the load-bearing half: an ID alone can only pay, so minting an account from one would be an escalation. Admin-toggleable.
- Password reset by emailed link, or an office re-invite when there is no mail. Staff can send a guardian a reset directly.
- Single sign-on from the OpenMasjidOS dashboard, treated strictly as an identity signal — on the masjid network, and from anywhere else through the dashboard's own two-step sign-in, which is the only way an admin gets in from outside.
- Sessions are server-side with signed, HTTP-only cookies; login is rate-limited with generic errors; changing a password signs out every other device but the one you changed it on.
- School name, contact details, an accent color, and a logo that appears on statements, on every email, and as the app's icon when it is installed on a phone.
- Currency per install (USD / CAD / GBP / EUR), and the date format the whole app displays and reads spreadsheet columns in.
- The wording on a family's printed sheet, so the office can say it their own way.
- Which OpenMasjidOS Stripe account to charge through, picked from the accounts the platform offers.
- External payments on or off — whether the kiosk and donation site may take tuition at all.
- Parent self-registration on or off.
- Nightly invoicing — on/off, the day of the month, and the due day.
- Mid-year billing floor — the period before which nothing may be generated.
- Past-due policy — the grace period, the smallest amount worth chasing, and how often.
- WhatsApp — on/off, the pause, which messages, country codes, the test student, the message wording, staff numbers, approved groups, and a log of what was queued.
- Year-view columns.
- Send a test email and send a test alert, so you can prove the plumbing works before you need it.
- A platform link status panel showing what the app can currently reach.
-
Payment notices to your notification channel can name the child (0.51.0). OpenMasjidOS forwards this app's routine notices to whatever channel the masjid set up — usually Slack or Discord — and those have always said "A tuition payment of $250.00 was received" and no more, because this app cannot see who is in that channel. A switch under Who to tell at the masjid sends the child's name instead. Off until you turn it on, admin only, audited both ways, and it covers payment notices and nothing else — no other notice this app sends there will ever name a family, whatever the switch says.
-
Transactional email through the masjid's OpenMasjidOS mail provider. This app has no SMTP of its own and holds no mail credentials — the platform owns the provider and the From address. A standalone install therefore sends nothing and degrades to links you can copy or print, which is a supported mode rather than a fault. Templates for invites, receipts, bills, autopay notices, card expiry, refunds, past-due reminders, password resets, alerts, and tests.
-
What parents are emailed is a setting, per message: a receipt every time they pay — however the money arrived, including cash, a check or Zelle recorded by the office, and payments made at the kiosk or on the donation site — a bill when it is ready, a heads-up three days before an autopay charge, a warning when a saved card is about to expire, a refund notice, a past-due reminder, and a notice when a card is declined. Invites and password resets are not switchable and always send: they are the only way a parent can reach their account.
-
A master pause for all parent mail, for an install being set up or a mistake about to become 200 emails.
-
Who at the masjid gets told. Add any address — the treasurer, the imām, a trustee — and tick which events it receives: autopay switching itself off, a declined card, a Student ID locked after repeated failed lookups, a payment recovered by reconciliation, a payment only partly recorded, a refund, the past-due digest, an account being ground by login attempts, the nightly invoice run, or every single payment. An address is not an account and grants no access. New addresses start on the events that cost money or hide a problem, because one email per payment is how somebody ends up muting the lot.
-
An alert can reach a person without the platform's help. Alerts fan out five ways — the addresses the office listed (the app's own email), the OpenMasjidOS alert channel, the masjid webhook for routine events, a staff member's own WhatsApp number, and a staff WhatsApp group — because any one of them can be silently dead.
-
Alerts name the child and the amount in the office's own copy, because a bill belongs to a child and a household total makes you go and look it up — and deliberately name nobody in anything sent to a third-party sink.
- Message parents on WhatsApp, through the masjid's own self-hosted gateway in OpenMasjidOS — nothing passes through an outside company. The platform owns the connection and one paced queue shared by every app; this app never touches a gateway.
- Seven parent messages, each with its own switch on both channels: a bill is ready, a receipt, a past-due reminder, an autopay charge coming up, an autopay failure, a card about to expire, and a refund. Everything with detail in it still goes by email; a WhatsApp is the short note that says to look.
- It starts switched off, paused, with nothing selected. Set a test student whose household receives messages — by email as well as WhatsApp — while everyone else stays quiet, watch a real one arrive, then lift the pause.
- Rewrite what every message says, with the family, the children, the amount, the balance and a payment link available as tags, and a preview of exactly what a real family will read.
- Staff alerts on a phone, per account, and to a WhatsApp group an admin approved for this app — a finance group that gets every payment. A group is a staff channel: no parent message can reach one, and by default a group's alerts name nobody.
- Parents opt out themselves from the portal, for either parent on the household — and the office can record it too, because a parent says "stop messaging me" at pickup, not by finding a toggle. Whoever sets it, nothing overrides the flag: not the pause exception, not the outreach button, not the test student. The trail records which side decided.
- One button asks the families with no email address for one, naming their children and explaining why, with the wording editable and a preview.
- One onboarding message explains what any of this IS (0.51.0) — every other parent message is about an event, and none of them says what a parent portal is or why an unfamiliar number is texting about fees. Sent to everyone, a course, a class or picked students, on both channels, in the madrasah's own wording. It carries no detail on purpose and points at the family sheet, and on WhatsApp it says which number the madrasah writes from.
- Nothing about signing in ever goes this way — invites, resets and verification links stay on email, which always works.
- Ask the app for the numbers by WhatsApp: an authorized admin messages
!studentsand gets this month's takings, what is outstanding and how many students are behind. It answers with counts and totals, never names — a chat keeps its copy forever — and it cannot change anything.
- A database snapshot every 30 minutes and at boot, written to the data volume — including on a standalone install, which is the one nobody else is looking after.
- Nightly invoicing at 02:00, autopay at 06:00, reconciliation at 07:00, and a public-address refresh every 15 minutes so an invite link is never minted against a stale address. A failed tick logs and the next one recovers; a standalone install schedules only what makes sense without a platform.
/healthzfor the platform's health checks.- Appearance inheritance — the app follows the OpenMasjidOS dashboard's light/dark theme, wallpaper and accent, relayed same-origin so a browser never has to reach the platform directly.
- What's new in the account menu after an update, built from the shipped changelog, so it works with no internet.
- Installable on a phone — a web manifest carrying the masjid's own name and logo, an iOS touch icon, and an install prompt on every signed-in surface. Parents get a home-screen icon; the office gets one too.
- Light and dark, RTL-aware layouts, reduced-motion honored, every string through i18next.
Half of this exists today. It arrives in phases, each one shipping on its own and leaving the app
fully working. The student record and admissions shipped in v0.52.0 and are marked ✅ below;
attendance, marks and hifz and report cards are written specifications with no code yet and
are marked 🚧. CLAUDE.md §4a carries the per-phase status and is the authority — a phase
reads "built" only once it has shipped.
✅ A real student record (shipped in v0.52.0). A student used to be a name, a Student ID, an optional date of birth and a status — a billing record, not a student record. It now keeps what a madrasa actually keeps: date of admission, date of withdrawal and why; prior schooling and any hifz progress on arrival; medical notes, allergies and emergency medical consent; and office notes with an author and a timestamp, which cannot be edited (a correction is another note) but can be deleted. The household keeps its address, the languages spoken and its nationality — once for every child on the record, the same way guardians already work. Every field can be switched off in Settings, which has tabs. The medical fields are admin-only and off until an office turns them on — the finance manager never sees them. Import matches: download your roster with your current data already in it, fill in the blanks, upload it back.
✅ Admissions (shipped in v0.52.0) — docs/ADMISSIONS.md. The funnel a
family actually walks: a short public inquiry form you can embed in the masjid's own website, a
waitlist the office orders by hand, and an admission form that reaches the family as a one-time
link — or stands open on a waiting-room tablet, which is local-only to the masjid network unless
you say otherwise. Completing it is what creates the student and mints their Student ID; an inquiry
never does. Re-admission is its own flow for the families you already have, folded into starting a
new school year: pre-filled from what you hold, the family edits what changed, and the office approves a
diff rather than a blind overwrite — in bulk, because it happens to a whole school in one fortnight.
Admission and re-admission fees are raised as ordinary charges through the machinery that already bills
everything else. With more than one school, an unrestricted admin routes an inquiry to the school that
will handle it, and that school's own staff see it from then on.
🚧 Attendance (specified, not built) — docs/ATTENDANCE.md. A daily register, not a timetable:
one class, one date, one named session, one mark — present, absent, late, or excused, with excused
a state of its own rather than a footnote on absent. It starts with a calendar that knows which
weekdays this madrasah teaches and which days it is closed, because closures move with the lunar
calendar and an absence rate computed against "every day in the term" is worthless. Whole class on
one screen, everyone present by default, one tap to change. And a view of which registers have
not been taken, because chasing that is the real job.
🚧 Marks and hifz (specified, not built) — docs/ACADEMICS.md. Subjects the install defines, each
graded, pass/fail or narrative; teachers and who teaches what; assessments with weights; marks where
"absent" is not a zero; and hifz as a running record — sūrah, āyah, juz, sabaq / sabqi / manzil —
rather than a score out of ten, because that is what it is.
🚧 Report cards (specified, not built). One grading scheme per install with the bands the madrasah writes, a card per student per term, and a draft → finalized lifecycle where finalizing freezes what the card showed. A mark corrected in March does not rewrite the card issued in January. Printable in the same way statements are, and visible to parents in the portal — finalized cards only.
Two things about it worth knowing up front. All of it is admin-only, and an admin's password works only on the masjid network — away from it, the register is reached by opening the app from the OpenMasjidOS dashboard, never by signing in at the public link. And class history starts the day it ships: the app has never recorded which class a child was in last term, and that cannot be reconstructed, so a term that predates the feature says "no roster history" instead of showing you a number that looks right.
Three roles. Checks are enforced server-side on every procedure, not in the UI.
| On the masjid network | Over the internet uplink | |
|---|---|---|
| admin — students, households, fee plans, structure, staff, settings | ✅ | ✅ only opened from the OpenMasjidOS dashboard — ❌ password sign-in |
| finance — billing, the ledger, payments, statements | ✅ | ✅ |
| parent — own household only | ✅ | ✅ |
An admin's password works only on the masjid network, at both login and session use — a cookie minted on the LAN is refused if it is later presented from the internet, and an admin password is never reset by emailed link. From outside, an admin gets in one way: pressing Open on Students in the OpenMasjidOS remote dashboard, which sits behind its own password and two-step sign-in. The platform vouches for that sign-in on every request, so signing out of the dashboard ends it. Clean walls elsewhere: finance never sees settings or staff management; parents can only ever reach their own household.
The academic layer keeps those walls and adds one. All of it is admin-only — so admissions, the register and the gradebook are worked on the masjid network, or from the OpenMasjidOS dashboard away from it. Finance never sees a medical field, an admissions record or a mark; parents see finalized report cards for their own children, and nothing else of it.
Install from the OpenMasjidOS App Store — one click, nothing to configure first. The app
serves its web interface on host port 8360 by default (the platform picks another free port if
that one is taken) and keeps everything in one Docker volume at /data — the SQLite database and
its snapshots, which is the whole of the app's state.
School name, currency and the Stripe account are all set up inside the app, on first run and in Settings — the manifest asks for no install-time answers.
Standalone-first. With no platform, no internet uplink, no Donations/Kiosk and no mail, the app still fully works on the masjid network: students, households, fee plans, invoices, charges, the ledger, statements, manual payments, nightly invoicing and the database snapshots all function. Every integration degrades gracefully rather than blocking.
Card payments need Stripe keys, which the app fetches from OpenMasjidOS server-to-server; the publishable key reaches the browser and the secret key stays in memory. Card details never touch this app's server — Stripe Elements handles them in the browser and we only ever see Stripe ids.
| Channel | Branch | Version | Image |
|---|---|---|---|
| Stable | main |
X.Y.Z |
…/openmasjidstudents:X.Y.Z, digest-pinned in the compose |
| Development | dev |
X.Y.Z-dev.N |
…/openmasjidstudents:X.Y.Z-dev.N — the exact build |
Pick the channel in OpenMasjidOS. Stable installs resolve an immutable @sha256 digest, so a moved
tag can never reach them.
Every dev build gets its own version and its own immutable tag, which is what makes the
development channel a channel at all: the platform detects an update by comparing versions, so an
entry that declared the same version as stable and pointed at a floating :dev tag left nothing to
detect and nothing to update to. One caveat worth knowing if you run the dev channel: a prerelease
sorts above its own final release, so 0.50.0-dev.3 → 0.50.0 is not detected as an update and a
host coming off the dev channel stays put until the next patch.
The most sensitive app in the org: records about children, reachable from the internet, moving money. The invariants that hold it together —
- An admin password is never accepted over the internet uplink, by policy, at login and at session use. From outside, the only admin is one the OpenMasjidOS dashboard's two-step sign-in vouches for on that very request.
- Card data never reaches our server; Stripe secret keys live in memory only, never on disk.
- A Student ID is a payment credential, and it is guessable — so the limiter is the control,
not secrecy: six failed lookups in an hour lock that ID and alert the office, and
identify,lookupand parent sign-up share one lockout bucket so failures cannot be laundered across them. Every mismatch answers identically, so nothing can be enumerated. IDs never appear in logs or in Stripe metadata. - The Fabric provider routes are secret-gated with a constant-time compare, validate before they do anything, and are never served to internet-origin requests.
- No PII in logs — ids and event names only, never a name beside an amount.
- Alerts carry two texts: the office's own copy names the child and the amount, because without that it is unactionable; anything going to a sink this app cannot see (a webhook, the platform alert channel) names nobody. An admin may lift that for their own webhook, for payment notices only — off until they do, and never for the platform alert channel.
- WhatsApp is treated as the weakest channel it is. Nothing auth-critical is ever sent on it — no invite, no reset, no one-time code — because the number can be banned overnight and that day must not be the day nobody can sign in. No Student ID, no card details, and no message body is ever logged or stored, on any path including failures. A WhatsApp command reply answers with counts and totals and never a name, because a chat keeps its copy forever and the phone holding it can change hands.
- Money is append-only: payments immutable, balances derived, reversals instead of edits, and foreign keys that refuse to delete anything money references.
- Invite / reset / verification tokens are CSPRNG, single-use, expiring, and stored hashed. The admission and re-admission links reuse that same machinery rather than a second one.
- There are no file uploads, anywhere — no student photos, no documents at admission, no payment
proofs. There is no upload path in the app at all, and any future one has six questions to answer
first (they are written down, in
CLAUDE.md§14). - Medical fields are admin-only and off by default, behind a server-side allow-list that decides per role which columns of a student record leave the server. A field nobody listed is visible to nobody, which is the safe direction.
- The public inquiry form is the only part of the academic layer reachable without signing in, and it stays that way. It answers identically whatever you submit, so it cannot be used to find out whether a child, an email or a family is already known; nothing submitted through it is ever written to a log.
/dataholds minors' PII and every payment record — treat backups of it accordingly.
Findings and remediation from the August 2026 audit are in docs/audit/.
All work happens on the
devbranch.mainchanges only at a release. Confirm withgit branch --show-currentbefore making changes — see the branching policy inCLAUDE.md.
npm install # all workspaces
npm run dev # server on :8080, web (Vite) on :5173 proxying /trpc /api /fabric
# /statements /sheets /invoices
npm run build # typecheck + build web and server
npm run lint # tsc --noEmit across workspaces
npm run test # vitest — the ledger and allocation invariants, the access walls,
# the Fabric contract, autopay, reconciliation, the origin policy, …
npm run image # build the container locally as :localOpen http://localhost:5173 in dev. Simulate an internet-origin request — and so exercise the
origin policy — by sending a cf-ray: header.
TypeScript everywhere in strict mode; Fastify + tRPC over SQLite (WAL) via Drizzle; React 18 +
Vite with Tailwind v4. All money is integer cents, and all of it flows through one ledger
module. The design system is ported verbatim from OpenMasjidOS for visual parity — see
packages/web/PORTED_FROM_OPENMASJIDOS.md. Strings go
through i18next with RTL-aware layouts; English ships today.
Docs: CLAUDE.md is the specification and the source of truth ·
docs/DATA_MODEL.md ·
docs/PAYMENTS.md ·
docs/FABRIC_BILLING_CONTRACT.md — the cross-repo contract ·
CHANGELOG.md
Created by Hasan Ismail, with immense help from Qari Ijaz and Osman Sayed.
Resources for this project were generously sponsored by An-Noor Institute, Rihlatul Ilm Foundation, and AsmaTec Inc..
May Allah reward everyone who made it possible.
Active development. See CHANGELOG.md for what has landed.
The academic layer (0.52.0 →) is half built: the student record and admissions shipped in
v0.52.0; attendance, marks and hifz and report cards are specified and not yet built.
CLAUDE.md §4a carries the per-phase status and is the authority, and
docs/ADMISSIONS.md, docs/ATTENDANCE.md and
docs/ACADEMICS.md are the specifications. Each says at the top which of it exists.
AGPL-3.0-only. Contributions are governed by the CLA — see CONTRIBUTING.md.





