Ghadaq Al‑Khayr
A private portal for the Al-Shabana family
A platform to document the family tree and manage memberships, join requests, messages and content. Visitors see a landing page only; members open the tree, documents and news; admins work with precise permissions.
Visit the site- Front end
- Next.js 16 · React 19 · TypeScript · Tailwind 4
- Data
- PostgreSQL 16, with optional Redis for rate limits
- Files
- Google Drive, with chunked uploads up to 250 MB
- Sign-in
- JWT session cookie, bcryptjs, instant session revocation
- Operations
- Docker behind Traefik and Cloudflare, daily backups
- My role
- The whole project, solo
01The problem
A large family needs one trusted tree that members update themselves, through requests that admins review. It holds sensitive data, like phone and ID numbers, that only authorized people should see.
The app and its database are hosted inside Saudi Arabia.
02How it fits together
Every request passes the proxy first. Nothing reaches the data without a valid session and the right permission.
03Three engineering decisions
Generations across in-law fathers
Children of a husband from outside the family take their generation from their mother. A recursive PostgreSQL query computes it, and a path array stops cycles.
WITH RECURSIVE maternal_tree AS (
SELECT child.id, mother.generation + 1 AS generation,
ARRAY[mother.id, child.id]::text[] AS path
FROM family_members child
JOIN family_members father ON father.id = child.parent_id
JOIN family_members mother ON mother.id = child.mother_id
WHERE father.is_external = TRUE
AND mother.is_external = FALSE
UNION ALL …Sensitive data by permission, not by role
Phone and ID numbers are never selected unless the user can edit members. Permissions are re-read from the database on every request, so revoking one applies at once. Documents stay private unless their owner shares them with the family.
const canSeeSensitive =
await hasAdminPermission(session, 'members:update');
const columns = canSeeSensitive
? `${PUBLIC_COLUMNS}, ${ADMIN_ONLY_COLUMNS}`
: PUBLIC_COLUMNS;Full names rebuild themselves
When an ancestor’s name changes, the full lineage names, “X bin/bint Y”, are rebuilt for every descendant, up to 25 levels deep.
const connector = member.gender === 'أنثى' ? 'بنت' : 'بن';
return `${firstName} ${connector} ${fatherName}`;04Security and operations
- Checks in CInpm audit, gitleaks for secrets, OSV for vulnerabilities, then lint, type checks and the build.
- BackupsA daily pg_dump to S3-compatible storage, with a restore test.
- MonitoringA health check every 5 minutes, with alerts on Telegram.
- Locked-down containerA multi-stage Docker image that runs as a non-root user.
05Known limits
06My role
I built the whole project alone: the interface, the API, the database, permissions, deployment and operations. It serves a family tree of about 2,000 people.
The code lives under the client’s GitHub account, so commits appear under their name.