← Back

Mobile App · iOS · Android · Web App · Local Gov

CILGD Member Portal

Status

Internal · Under Development

Stack

React NativeExpoSupabasePaystackDeno Edge Functions

[ THE THESIS ]

Membership infrastructure fails when payment truth lives on the client.

CILGD replaces paper forms, WhatsApp approval chains, and manual bank transfers with a mobile-first system where registration, dues, and access control resolve through verified server-side events. The architecture treats every Paystack webhook as untrusted until HMAC verification proves it, so membership status is earned by payment truth instead of frontend state.

INTERNAL · UNDER DEVELOPMENT · CILGD · Local governance professional body

Technical proof
6
roles in RLS
super_admin → guest
3
payment rails
Paystack · FW · pawaPay
12
specialisations
constrained in schema
BoG
2026-aligned KYC
admin artefacts modeled

SECTION 01DESIGN PRINCIPLES

Five constraints that turn a membership body into a Ghana-first platform

Role: Developer & Technology Adviser. CILGD owns the product; I architected and built the technology.

01

Supabase-first by design

No Express/Django server. Anything the client cannot do runs as an Edge Function (Deno) or a Postgres RPC. Fewer moving parts, smaller blast radius.

02

RLS on every table · zero trust

Members can only read their own profile, their own documents, their own payments. Admins are elevated through a role column enforced in policies, not in client code.

03

Payments at the edge · Ghana first

Paystack owns MTN + Telecel MoMo natively — that is the payment rail most international platforms ignore and the one that kills adoption in Ghana. So it ships as the primary rail, not an afterthought.

04

Offline-resilient onboarding

The 6-step wizard persists state locally so a flaky mobile connection doesn't lose a professional's 10-minute application. Progress is recoverable.

SECTION 02SYSTEM TOPOLOGY

Three environments cooperate — one rail leads

Client · Expo SDK 54 · iOS / Android / Web

Auth

email + Google

Onboarding

6-step wizard

Membership

tiers + status

Payments

Paystack WebView

Settings

themes + profile

Session

@supabase/supabase-js v2 · AsyncStorage + expo-secure-store (iOS Keychain) · 1-hour JWT with auto-refresh

Supabase Platform · Auth · PostgREST · Realtime · Storage · Edge · pg_cron

9 key tables

  • • profiles · membership_grades
  • • fee_schedule · payments
  • • documents · courses
  • • cpd_entries · events
  • • audit_log

6-role RLS

  • • super_admin
  • • admin
  • • finance_admin
  • • member · pending_member
  • • guest

Deno Edge Fns

  • • initialize-payment
  • • paystack-webhook (HMAC)
  • • pg_cron · renewal checks
  • • good-standing downgrades
HMAC-signed webhooks

Paystack · Primary

MTN + Telecel MoMo + cards

Ghana cedi settlement · launch-ready

Flutterwave · Next

Diaspora cards

USD / GBP for UK + US members

pawaPay · Fallback

Pan-African MoMo

KE · UG · RW · NG · more

SECTION 03THE SIX-STEP ONBOARDING WIZARD

Every step is persisted to Supabase before the next one loads

A dropped connection in step 4 never wipes steps 1–3.

[ Application State Machine ]

Six gates. One recoverable member profile.

Each screen writes a checkpoint before the next one opens. That makes onboarding feel mobile-native even when the applicant is on an unstable connection.

Persistence

Step-by-step

Failure mode

Resume safely

01

Personal details

Name, DOB, country, city, nationality, gender, profile photo — PersonalDetailsStep.

02

Professional details

One of 12 specialisation domains, highest qualification, years of experience, employer, job title — ProfessionalDetailsStep.

03

Grade selection

System computes eligible tiers from step 2's rules (e.g., Doctorate + 3 years → MCILGD). User picks what they're applying for — GradeSelectionStep.

04

Ethics Affirmation — mandatory

Digital signature of the CILGD Code of Ethics. profiles.ethics_affirmed = true, ethics_affirmed_at = now(). Cannot be skipped.

05

Document upload

Ghana Card (front + back), ORC certificate, academic transcripts, professional certifications → RLS-scoped documents bucket.

06

Review & submit + pay

Read-only snapshot + initial registration fee via Paystack. On success the profile drops into pending_member awaiting admin approval.

SECTION 04MEMBERSHIP TIERS

Five tiers · real qualification rules · upgrade_grade runs server-side

Progression is code, not vibes. A Postgres RPC checks the rules and auto-promotes.

Tier
Post-nominal
Rule
Discount
Governance
Fellow
FCILGD
Doctorate + 8 yrs OR Masters + 15 yrs
30%
Can contest for President
Full Member
MCILGD
Doctorate + 3 · Masters + 5 · Degree + 8
20%
Any role except President
Associate
ACILGD
Masters + 3 · Degree + 5 · or Advanced Diploma
10–20%
Full voting rights at AGM
Graduate
Graduate Member
Degree or Diploma · any discipline
Access to progression engine
Student
Student Member
CILGD candidate / recognised institution
Mandatory dues + progression path

SECTION 05ENGINEERING HIGHLIGHTS

Three decisions that matter for a government-grade platform

COMPLIANCE

Bank of Ghana 2026 KYC modeled server-side

Admin KYC artefacts (ORC certificate, Ghana Card front + back, recent CILGD bank statement) are first-class objects in the schema, filed to the RLS-scoped documents bucket, readable only by finance_admin and super_admin.

regulator-ready out of the box

AUTHORIZATION

Six-role RBAC enforced at the database

Roles are checked in Postgres RLS policies, never in client if-statements. A compromised frontend cannot elevate itself. Finance cannot read raw auth tokens; members cannot read other members.

never-in-the-client

PAYMENTS

Signed HMAC webhooks verified inside Deno Edge

Paystack signs every webhook; paystack-webhook verifies the signature before flipping any payments row to success. Replay attacks, forged events, and tampered amounts all fail at the edge.

verify before you believe

SECTION 06CODE PROOF

The webhook that turns a Paystack charge into a tier promotion

Verbatim Deno Edge Function — signature check first, database writes second.

supabase/functions/paystack-webhook/index.ts

typescript

// supabase/functions/paystack-webhook/index.ts
import { createHmac } from "node:crypto";

Deno.serve(async (req) => {
  const raw = await req.text();
  const sig = req.headers.get("x-paystack-signature");
  const expected = createHmac("sha512", PAYSTACK_SECRET)
    .update(raw)
    .digest("hex");

  if (sig !== expected) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(raw);

  if (event.event === "charge.success") {
    // Flip the pending row to success and fire upgrade_grade.
    const { data: payment } = await supabase
      .from("payments")
      .update({ status: "success", paid_at: new Date().toISOString() })
      .eq("reference", event.data.reference)
      .select()
      .single();

    if (payment?.fee_type === "tier_renewal") {
      await supabase.rpc("upgrade_grade", { user_id: payment.user_id });
    }
  }

  return new Response("ok");
});