9.8 KiB
Africa Alert PWA — Project Conventions
This document captures the load-bearing conventions for the Africa Alert PWA. Each rein's agent.md links here instead of inlining these rules. Keep this file short and operational.
Tech stack (lock these in)
- Frontend: React 18 + Vite + TypeScript,
react-router-domv6,axios,zustand,recharts,lucide-react,vite-plugin-pwa. - Backend: Node.js + Express,
better-sqlite3(NOTsqlite3),jsonwebtoken+bcryptjs(NOT nativebcrypt),multer,@supabase/supabase-js(used by SyncEngine). - DB: SQLite, WAL mode,
foreign_keys = ON. - Cloud: Supabase REST API (
https://api.next_gen.techarvest.co.zw). - Payments: Paynow (Zimbabwe).
- Deploy: Docker Compose (multi-stage
Dockerfile).
Directory layout (don't deviate)
africa-alert-pwa/
├── client/
│ └── src/
│ ├── App.tsx # router + ProtectedRoute + role-based getRoutes()
│ ├── main.tsx
│ ├── components/ # Nav, PaynowPayment, shared UI
│ ├── pages/ # feature pages
│ │ ├── admin/ # admin-only pages
│ │ ├── teacher/ # teacher-only pages
│ │ ├── student/ # student-only pages
│ │ ├── parent/ # parent-only pages
│ │ ├── dashboard/ # role landing pages
│ │ └── exams/ # exam module (canonical reference)
│ └── store/ # Zustand slices (auth, api, exams, …)
├── server/
│ └── src/
│ ├── index.js # Express bootstrap, registers 12 controllers
│ ├── controllers/ # one file per resource
│ ├── services/
│ │ └── SyncEngine.js # bidirectional SQLite ↔ Supabase
│ └── database/
│ └── init.js # schema, idempotent
├── data/ # SQLite file (gitignored)
├── uploads/ # multer uploads (gitignored)
├── docker-compose.yml
├── Dockerfile # multi-stage
└── .harness/ # team definition (committed)
├── agent.md # orchestrator
├── reins/ # specialists
├── docs/ # project conventions
└── changelogs/ # daily change log
SQL contract (every table must have these)
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT UNIQUE, -- stable cross-system id
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_synced_at DATETIME,
sync_status TEXT DEFAULT 'pending' CHECK(sync_status IN ('synced', 'pending', 'conflict')),
is_deleted INTEGER DEFAULT 0
Adding a table is a two-step commit:
- Add the
CREATE TABLE IF NOT EXISTSblock inserver/src/database/init.js(backend-expert). - Append the table name to
tablesToSyncinserver/src/services/SyncEngine.js(sync-expert) — in dependency order (parents before children).
Controller template (canonical)
const express = require('express');
const path = require('path');
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH || path.join(__dirname, '../../../data/school.db'));
db.pragma('foreign_keys = ON');
const router = express.Router();
// inline auth middleware (or use the one from server/src/index.js)
const auth = (req, res, next) => { /* … */ };
// CRUD handlers — all writes set sync_status = 'pending'
router.get('/', auth, (req, res) => { /* … */ });
router.post('/', auth, (req, res) => { /* … */ });
// …
module.exports = router;
Register in server/src/index.js:
app.use('/api/<x>', require('./controllers/<x>.controller'));
Alphabetical order, please.
Frontend store template (canonical)
// client/src/store/<x>.ts
import { create } from 'zustand';
import { api } from './api';
interface XStore {
items: any[];
currentItem: any | null;
fetchItems: (filters?: any) => Promise<void>;
createItem: (data: any) => Promise<any>;
updateItem: (id: number, data: any) => Promise<void>;
deleteItem: (id: number) => Promise<void>;
}
export const useXStore = create<XStore>((set, get) => ({
items: [],
currentItem: null,
fetchItems: async (filters) => {
const params = new URLSearchParams(filters || {});
const { data } = await api.get(`/<x>?${params}`);
set({ items: data });
},
createItem: async (data) => { /* … */ },
updateItem: async (id, data) => { /* … */ },
deleteItem: async (id) => { /* … */ },
}));
Frontend page template (canonical) — verified 2026-06-12
A new page needs five coordinated edits, not just one file. Skipping any of them is a defect. The frontend team has shipped this checklist twice (exams module, then the 2026-06-12 attendance + front-office extension) — it works.
- Store at
client/src/store/<x>.ts(shape above). - Page at
client/src/pages/<role>/<Page>.tsx(or cross-role underclient/src/pages/). Controlled inputs withuseState; no new form lib. - Route in
client/src/App.tsx— add the{ path, element, roles: [...] }entry inside the correct role'sswitcharm ofgetRoutes().ProtectedRouteis the role gate; do not checkuser.roleinside the page. - Nav entry in
client/src/components/Nav.tsx— add to the matchingNAV_CONFIG.<role>array(s). Icons fromlucide-react; no new icon lib. - Changelog entry in
.harness/changelogs/YYYY-MM-DD-<topic>.mdlisting every new file + the roles that see each route. Commit it on the branch.
If a page is used by more than one role, extract the shared view into
client/src/components/ and have each role's page be a thin wrapper that
loads the role-specific data source (e.g. teacher loads all students via
/api/users?role=student; parent loads linked children via
/api/users/children).
Parallel multi-context store pattern (unified slice, context-routed)
When the same operation (take attendance, list records, …) has to hit
different backend endpoints depending on a runtime context, don't create
one slice per context. Create a single slice that takes a context enum
and routes internally:
type AttendanceContext = 'class' | 'hostel' | 'transport' | 'club';
const baseFor = (ctx: AttendanceContext): string =>
ctx === 'class' ? '/attendance' : `/${ctx}-attendance`;
// fetchRoster / submitBulkAttendance / fetchHistory all switch on ctx
// and call api.get/post(`${baseFor(ctx)}/...`).
Reference: client/src/store/attendance.ts (2026-06-12, attendance + front-office
extension). Pair with the multi-tab page pattern (single page, top tabs that
swap the picker + roster source + submit endpoint).
Dev port drift (corrected 2026-06-12)
vite.config.ts in this repo runs the dev server on port 3000, not the
5173 mentioned earlier in this file. The API still lives on 3001. Update
any other doc or tool that still says 5173.
Auth + RBAC
- JWT signed with
process.env.JWT_SECRET(default for dev only, loud warning). - 7-day expiry.
- Roles:
admin,teacher,student,parent,accountant,librarian,nurse. - Backend: every protected route goes through
authmiddleware; admin-only writes guard withreq.user.role !== 'admin'. - Frontend: route gating in
App.tsxvia<ProtectedRoute allowedRoles={[…]}>— do not gate inside pages.
Dev commands
# Backend
cd server
npm install
npm run db:init # runs server/src/database/init.js (idempotent)
npm run dev # nodemon, port 3001
# Frontend
cd client
npm install
npm run dev # Vite, port 5173 (proxies /api → localhost:3001)
npm run build # production build → client/dist/
npm run preview # serve built bundle
# Full stack (production-like)
docker-compose up --build
Sync engine contract
- Singleton:
getSyncEngine(). - Default interval: 30s (
SYNC_INTERVALenv var, in ms). - Tables listed in
tablesToSync(server/src/services/SyncEngine.js), in FK dependency order. - Push:
sync_status = 'pending' AND is_deleted = 0, limit 100/table. - Delete:
is_deleted = 1 AND sync_status = 'pending', limit 50/table. - Pull:
updated_at >= last_sync. - Conflict policy: server-wins, always.
- Offline mode: empty
SUPABASE_KEY→ cycle no-ops, app keeps working. - HTTP helper: raw
http/https(nofetch/axios), 10s timeout.
Payments (Paynow)
- Endpoints:
POST /api/payments/initiate,GET /api/payments/status/:id,POST /api/payments/webhook,GET /api/payments/student/:id,DELETE /api/payments/:id. - Webhook handler is idempotent (Paynow retries).
- Payment +
student_fees.statusupdate wrapped indb.transaction(() => { … })(). - Env vars:
PAYNOW_INTEGRATION_ID,PAYNOW_INTEGRATION_KEY,PAYNOW_RETURN_URL,PAYNOW_BLOCKING_URL.
Env vars (.env.example is the source of truth)
PORT=3001
JWT_SECRET=change-me-in-production
DB_PATH=./data/school.db
SUPABASE_URL=https://api.next_gen.techarvest.co.zw
SUPABASE_KEY=
SYNC_INTERVAL=30000
PAYNOW_INTEGRATION_ID=
PAYNOW_INTEGRATION_KEY=
PAYNOW_RETURN_URL=http://localhost:3000/fees
PAYNOW_BLOCKING_URL=http://localhost:3000/api/payments/webhook
.env is gitignored. Never commit it.
Roles to remember (the people, not the agents)
- admin — full access, user/department management, reports, settings.
- teacher — own classes, attendance, assignments, grades for assigned students.
- student — own courses, assignments, grades, attendance read.
- parent — own child's academic progress, attendance, messages, fees, calendar.
- accountant / librarian / nurse — role-specific scopes; consult schema for column-level access.