Operations system
Da’wa, Guidance and Community Awareness Association, Al-Majmaah
An internal system built around the association’s operational plan. Each department logs its work on its own page, and management sees targets against results in one dashboard, with finance, donors, official letters and an evidence archive alongside.
- Front end
- React 19 · Vite · Tailwind 4 · PWA
- Server
- Google Apps Script with a single endpoint
- Data
- Google Sheets, with evidence files on Google Drive
- Hosting
- Firebase Hosting with strict security headers
- Protection
- ChaCha20 encryption and PBKDF2 hashing, written without libraries
- My role
- The whole system, solo
01The problem
Six departments work on one yearly plan. Each needs to log its work without seeing or changing anyone else’s, and management needs one picture that compares targets with results.
The board and donors need limited, read-only access without full accounts.
02How it fits together
The interface sends only what changed, and the server decides who can read and write before anything is saved.
03What it runs
- Plan and operationsThe quarterly plan, each department’s operations, and an executive dashboard of targets against results.
- FinanceInvoices, payment orders, receipt vouchers, bank accounts and a chart of accounts.
- DonorsDonor management and proposals, plus a portal where each donor sees only their projects.
- CorrespondenceLetters with an approval and official-stamp workflow, circulars, announcements with read receipts, and chat.
- Preachers and recordsA dossier per preacher with residency-expiry tracking, and encrypted records.
- Evidence and reportsAn evidence archive on Drive, and executive reports exported to multi-sheet Excel.
04Three engineering decisions
A department stays inside its lane
Permissions are checked on the server on every write: staff write only in their own department and can’t move an item to another one. Once a letter is approved its department can no longer edit it, and only the director or a delegate can stamp it.
if (!isWholeOrg_(ctx)) {
if (oldItem && !ownsDept_(ctx, p.depts(oldItem))) return 'OUT_OF_SCOPE';
if (newItem && !ownsDept_(ctx, p.depts(newItem))) return 'OUT_OF_SCOPE';
// لا يجوز نقل عنصر إلى قسم آخر
if (oldItem && newItem &&
JSON.stringify(p.depts(oldItem)) !== JSON.stringify(p.depts(newItem)))
return 'OUT_OF_SCOPE';
}Never overwrite another department’s item
If a new item arrives with an ID that’s already taken, or that belongs to an item the user can’t see, the server renames it instead of writing over it, and tells the interface the new ID.
if (existing && (isNew || !oldItem || !canReadItem_(ctx, collection, oldItem))) {
var oldId = item.id;
do { item.id = oldId + '-' + hex_(randomBytes_(2)); }
while (dataGet_(collection, item.id));
result.renamed.push({ from: oldId, to: item.id });
}Encryption without libraries
Apps Script has no complete crypto library, so I wrote ChaCha20 and PBKDF2 by hand and tested the cipher against the official RFC 8439 vectors. Sign-in takes the same time whether a number is registered or not, so timing doesn’t reveal accounts.
function pbkdf2_(passwordBytes, saltBytes, iterations) {
var u = hmac_(passwordBytes, saltBytes.concat([0, 0, 0, 1]));
var t = u.slice();
for (var i = 1; i < iterations; i++) {
u = hmac_(passwordBytes, u);
for (var j = 0; j < t.length; j++) t[j] ^= u[j];
}
return t;
}