A small internal-tools app: reviewable back-office queues sharing one governed foundation.
Authentication, role-based access control, an append-only audit log and maker-checker approval are
written once in lib/, and every queue consumes them. Adding a queue never means reimplementing any of
them.
[AT MINUTE 100-110, CONFIRM THIS IS STILL TRUE BEFORE LEAVING IT IN] Two queues are built. Refunds: [one line on what the refunds queue shows and does]. Payout approvals: [one line, written after the queue exists].
Node 22. Built and run on v22.21.1.
git clone https://github.com/mathew-builds/internal-tools-platform.git
cd internal-tools-platform
npm install
npm run devThe database is created, migrated and seeded on first boot. There is no migrate step and no environment file.
The maker-checker rule is the point of this codebase, and it is invisible unless you go looking. Five steps, using the role switcher:
-
Switch to the
approverrole. The queue shows who requested each refund. -
Open a pending refund that the approver requested, and approve it. It is refused.
-
Open a pending refund that someone else requested, and approve it. It succeeds.
Steps 2 and 3 are the same user, in the same role, pressing the same button. That is what makes step 2 the maker-checker rule rather than a permission error or an approve path that is broken for everyone. Run them in that order, and do not accept the refusal in step 2 on its own.
-
Switch to the
requesterrole and raise a request, to see the other half of the loop. -
Switch to the
auditorrole, open the refund you approved in step 3 and open its audit view. The request and the approval are both there, with the actor on each row.
The audit table has BEFORE UPDATE and BEFORE DELETE triggers that RAISE(ABORT), so any UPDATE or DELETE that goes through the application fails with a constraint error instead of silently succeeding. That is the engine refusing it, not my code paths remembering not to.
I did not build a hash chain inside the two hours. It belongs on the audit row, chaining each entry to the previous one so that tampering is detectable even where the engine permits it, and it is the first thing I would add next.
What this is not is access control. SQLite has no GRANT or REVOKE, and any process that can write the file can drop the trigger. So in SQLite this is protection against my own bugs, not against a privileged actor. On Postgres I would own the table as a different role and grant the app INSERT and SELECT only, and that is the upgrade path.
This logs writes. It does not log reads, and who viewed this refund is the question your regulator will actually ask. That is a second table, a middleware hook on every read path and a retention decision, and it is one of the reasons I am not telling you to rebuild KYC.
- The KYC review queue. The hard part there is document storage and a retention schedule, not the UI, and this foundation makes neither of them cheap. A queue needing document upload or retention does not fit the pattern, and the foundation says so rather than shipping a partial version.
- The feature-flag admin panel. The recommendation is to retire it to a dedicated flag service, not to rebuild it here.
A queue is a set of records moving from pending to approved or rejected, where the person who requested the change cannot be the person who approves it.
new-reviewable-queue.devin.md at the repository root is the Playbook that generates one. Attach it to a
session and give it the entity name and its fields. It imports getCurrentUser, can, appendAudit and
assertDifferentActor rather than reimplementing them, so a new queue is a migration, a route folder,
and one entry in lib/nav.ts.
The boundary is explicit: a queue whose records need document upload, document storage or a retention policy does not fit this pattern and does not get cheap.