Enterprise Security & Compliance Platform
In production
Backend & front-end engineer · Feb 2025 – Aug 2026 · Commercial employer ·
described at a high level, without product names or internal detail
A multi-tenant cybersecurity and compliance SaaS sold to companies that must
prove where their employees’ data physically lives and control who inside the organisation
can see what. I worked across the whole system: the event-driven Python services that hold the
data, and the federated React front end that presents it.
On the backend that meant an org tree — company, group, branch, department, employee —
where every score, report and policy decision is scoped to the caller’s own tenant and slice.
Services never call each other for domain data; every cross-service fact arrives as a Kafka event
and is materialised into a local read model. I also built a
multi-region data-residency control plane that moves an entire tenant’s data
between geographic zones on demand, with four fail-closed regions and an eight-state migration
lifecycle.
The last of these services is a metered AI assistant: it answers questions from
live product data through nineteen hand-written tools, and it holds no service credential of its
own — every tool call goes out with the asking user’s own token, so the product API stays
the only authorization boundary and the assistant can never read a row that person couldn’t
open themselves. Billing a streaming answer whose cost isn’t known until it finishes meant a
reserve-then-commit ledger: set aside the worst case before the first token, commit the real figure
at the end, so a balance can’t go negative and an abandoned stream expires instead of quietly
eating credit. Across the eight services I was sole or principal author of seven and co-developed
the eighth with a colleague.
The hard part
Both product homepages rendered a white screen on iPhone — and only on iPhone.
Nothing returned a 500, no build failed, curl was green, and no React error
boundary ever fired.
I traced it to Module Federation shared-dependency drift. Every app carried
^19.2.x, so each npm install had resolved a different patch of React.
Federation collapses a shared singleton down to one copy — and whichever copy won the load
race got paired with another app’s renderer, throwing React error #527 before anything could
mount. Desktop Chrome happened to win the race; WebKit did not. The fix was to pin every shared
singleton to one exact version across all four codebases, move the federation share strategy to
loaded-first so the entry chunk stops waiting on a preload of every remote, and add
a version assertion to the deploy scripts so the drift can never come back silently. I reproduced
it with Playwright’s WebKit engine, because Chromium never showed the bug.
- Python 3.13
- FastAPI
- SQLAlchemy 2
- Pydantic v2
- PostgreSQL 17
- Apache Kafka
- Redis
- React 19
- Module Federation
- Vite
- TypeScript
- i18next
- Anthropic Claude
- Google Gemini
- PM2
- nginx
- GCP
- Services
- 8
- Endpoints
- 778
- Tables
- 155
- Kafka consumers
- 55
- Front ends
- 9
- Locales
- 10
Company code lives on a private GitLab, so it isn’t linkable. Sign-in, the API gateway,
DNS and TLS were owned by teammates — I built the services and the product front ends that
sit behind them. Happy to walk through any of it in an interview.
Offia’s Studio
Live
Sole developer · Jun – Aug 2026 · My own web practice ·
offiastudio.tech
The storefront for my own web-design practice. A business owner can click through
twelve working sample websites built for their own industry, submit a project
brief that an AI helper will tidy up for them, and then follow the build on a live status page
with a delivery countdown, a deliverables vault and a support thread.
Because the person editing the site isn’t a developer, ninety-six pieces of public copy
— plus services, pricing, portfolio, testimonials, FAQs and navigation — are editable
from an admin dashboard rather than a code deploy. The content editor mounts the
real public page components inside the admin shell and feeds them the unsaved draft, so
what you preview is the actual page, not a re-implementation of it.
The hard part
I audited the app before launch and found a way to make myself an administrator.
The signup trigger copied the new user’s role straight out of
raw_user_meta_data — which is attacker-controlled input, because anyone can
pass arbitrary metadata to signUp(). Any visitor could have registered as an admin.
I fixed it in depth rather than at one layer: the trigger now hard-codes
role = 'client' and deliberately does not update the role on conflict, so
re-registering can’t escalate or downgrade an existing account; a separate guard trigger
reverts any client-side attempt to write the column; and the row-level INSERT policy pins all
eight privileged fields, so a client can only ever create a clean, owned, pending request. That
was one of twelve findings I wrote up and remediated in a migration before the site went out.
- React 18
- TypeScript
- Vite 5
- Tailwind
- TanStack Query
- Framer Motion
- Supabase
- PostgreSQL + RLS
- Vercel serverless
- Google Gemini
- Lines of TS
- 38,556
- Tables
- 21
- RLS policies
- 57
- Routes
- 43
- Demo sites
- 12
- Serverless fns
- 4
No application server: the browser talks to Postgres directly and every authorization decision
is a row-level-security policy. The twelve demo sites are self-contained samples with mock data,
not delivered client work.
Luamé
Live · client
Sole developer · Jun – Jul 2026 · Braiding salon, Mississauga ON ·
luame.vercel.app
A braiding and natural-hair salon in Mississauga was taking every appointment through Instagram
DMs: no published availability, no way for a customer to see which slots were already gone, and a
real chance of promising the same hour to two people. Luamé is a public booking site plus
an owner-only portal. The customer picks a style, sees live availability, books, and confirms over
WhatsApp — deliberately, because that is the channel her customers actually use. The owner
accepts, reschedules or completes in one tap, edits her own price menu, and blocks days off.
It is live and serving her real prices today.
The hard part
Two customers can open the booking page at the same moment, see the same free slot, and
both submit. A check-then-insert in JavaScript loses that race, and the owner turns up
to a double-booked Saturday.
I made the database the arbiter instead of the client: a partial unique index on
(appointment_date, appointment_time) restricted to active statuses, so Postgres
rejects the second insert atomically — and because the index is scoped to
pending and confirmed, cancelling a booking frees the slot again with
no extra code. The UI catches the 23505 unique violation, re-fetches availability
and tells the customer the slot just went. Showing live availability then collided with the
privacy model, since row-level security correctly stops one customer reading another’s
bookings — so a SECURITY DEFINER function returns only the busy time strings,
and by its return type alone can never emit a name, an email or a phone number.
- Next.js 14 App Router
- React 18
- TypeScript
- Tailwind
- Supabase Auth
- PostgreSQL + RLS
- Vercel
- Tables
- 6
- RLS policies
- 13
- Routes
- 10
- Custom API endpoints
- 0
Zero API endpoints is the design, not a gap — server components and the browser client read
Postgres directly under RLS. WhatsApp is free click-to-chat deep linking, not the Business API.
RankAfrica
Live · pre-launch
Sole developer · Jun – Aug 2026 · Client project ·
rankafrica.org
Nigerian institutions grade on 4.0, 5.0, 7.0 and raw-percentage scales with very different rigour,
so a 4.2/5 at one school cannot honestly be compared to a 4.7/5 at another. RankAfrica standardises
every credential onto one 100-point index, adds discipline-appropriate evidence
scoring on top, and publishes a leaderboard that vetted recruiters can search. Students build their
own profile and apply to opportunities; administrators run submission cycles, verify evidence and
approve who appears publicly. One repository deploys as two separate projects — a FastAPI
backend and a React front end.
The hard part
A flat ratio conversion would have let a weak school’s First Class outrank a strong
school’s. I wrote a five-stage equivalency engine instead: resolve the native
grading scale, classify into the local class-of-degree band, project onto a WES-style US 4.0 band
by piecewise-linear interpolation rather than a ratio, apply an institutional-rigour factor with
a floor so no school can be scored into the ground, and emit an auditable breakdown at every stage.
A second problem was reproducibility: four students on an identical 38.5 were being rendered as
ranks 1, 2, 3, 4 from an unstable sort. I rewrote rank assignment as standard competition ranking
— 1, 2, 2, 4 — with the student id as a deterministic secondary key, and reused that
one routine across all four ranking scopes. The AI-scored components are computed once and stored
on the row, so a full leaderboard recompute is pure arithmetic with zero model calls.
That single decision is what makes recomputing the whole board affordable.
- Python 3.11
- FastAPI
- SQLAlchemy 2
- Pydantic v2
- PostgreSQL
- JWT + RBAC
- React 18
- Vite
- Zustand
- OpenAI gpt-4o-mini
- Endpoints
- 149
- Tables
- 28
- Front-end routes
- 48
- Lines of Python
- 12,609
Publicly live and pre-launch: the leaderboard stays hidden behind an administrator toggle until
the first submission cycle closes. Authorization here is FastAPI dependency checks, not RLS —
the browser never touches Postgres.
PaceRace
MVP · 139 tests
Sole developer · May – Aug 2026 · Personal product · FastAPI + Expo React Native
Fitness trackers are asynchronous: you record a run, upload it, and compare numbers afterwards.
PaceRace makes it synchronous — several people start the same distance at the same moment and
everyone sees a live per-second leaderboard of who is ahead while they are still
running. Solo athletes race a “ghost” replayed from their own fastest previous effort.
The backend and the mobile app share a hand-written TypeScript mirror of the Pydantic WebSocket
contract, so a change to a message type breaks the build rather than the race.
The hard part
N runners each sending GPS once a second naively produces N² socket writes per
second, per race — and every one of those writes costs somebody’s phone
battery.
I capped it in two places. Each participant is admitted at most once per second, and a single
background ticker per race broadcasts a coalesced leaderboard only when the state is
actually dirty — so the broadcast rate is one per second for the whole race no matter how
many people are in it. Across multiple server workers, frames fan out over Redis Pub/Sub with a
per-worker origin tag, so a worker skips its own re-published frames and every client receives
each message exactly once. A subtler bug came later: finishing positions were being assigned by
the order frames arrived, so a slower runner on a faster connection could be ranked
ahead. Positions are now derived from finish time, in both the in-memory hub and the
database re-rank — because those two have to agree.
- Python 3.11
- FastAPI
- WebSockets
- async SQLAlchemy 2
- PostgreSQL 15
- Redis 7 Pub/Sub
- Alembic
- Docker Compose
- Expo / React Native
- TanStack Query
- pytest
- mypy --strict
- Automated tests
- 139
- Routes
- 28
- Tables
- 10
- Lines of code
- 12,100
72 pytest + 67 Jest tests, all passing, with mypy --strict clean across 48 source
files — this is the project where I own the test suite. Complete MVP; not deployed and not
submitted to an app store.
Amarex
Built
Sole developer · Jul 2026 · Client project · Next.js 14 + Supabase + Paystack
A paid trading-education membership platform. Paystack collects the subscription, a private course
library and live market signals sit behind it, and Claude turns each uploaded course PDF
into a per-student assessment that is timed, monitored by a browser extension, auto-graded
and then reviewed by a human. Rejecting a paper mints a brand-new personalised set from the same
question bank, so a retake is never the same exam twice.
The hard part
In an architecture where the browser queries Postgres directly, an exam answer key is one
hand-written request away from the student sitting the exam. Row-level security
wasn’t enough, because the row a student is permitted to read is the row that contains the key.
I moved the boundary from rows to columns. The question bank is fully revoked from the anonymous
and authenticated roles, and the assignment table has ALL revoked with an explicit
GRANT SELECT on only its ten non-key columns — so select('*')
raises permission denied for everyone, and the key can be read only by the server route
that grades the paper. The same instinct governs money: the price always comes from the
plans table rather than the request body, and Paystack is verified twice server-side
— an HMAC-checked webhook and a return-trip verify — both converging on one idempotent
activation function, so a closed tab or a dropped webhook still activates exactly once.
- Next.js 14 App Router
- React 18
- TypeScript
- Radix UI
- Supabase Realtime
- PostgreSQL + RLS
- pg_cron
- Paystack
- Anthropic Claude
- Zod
- Chrome MV3 extension
- Tables
- 19
- RLS policies
- 49
- Editable copy fields
- 125
- Zod schemas
- 23
- Scheduled jobs
- 3
Built and type-clean, awaiting the client’s production environment. The proctoring extension
is a deterrent that records ten event types into a reviewable timeline — it is not, and is not
claimed to be, tamper-proof invigilation.