◉rex os
●building in public
// build logs

Building in
public.

Every decision, mistake, and milestone — documented as it happens across all active projects. No polished post-mortems. Just the real process.

Active · Updated regularly
5 active projects
40 entries logged
✎ Add Entry
// project
// tag
🌐
Portfolio Website
This site — building in public from scratch◐ building10 entries
Day 01Feb 2026

Started the Portfolio — First Version Was Embarrassing

builddesign

Decided to finally build a proper portfolio. Not one of those "here are my projects" pages with a headshot and a LinkedIn button — something that actually does the work of selling me while I sleep.

Spent the first hour convincing myself to stop overthinking the design and just start. Went with a dark navy background (#0F172A), blue accent (#3B82F6), Syne for headings. That took longer to decide than it should have.

Built the homepage — hero section, stats with animated counters, tech stack grid, projects section, how I work, and a CTA. By the end of the session it looked real. Like something I'd actually want to show someone.

The scroll reveal animations broke twice because I forgot IntersectionObserver runs on the client and the component was doing SSR things it shouldn't. Classic. Fixed it by checking typeof window === 'undefined' before running the observer.

First commit pushed. It's rough but it's something.

// decision
Decided to position this as a conversion tool, not a portfolio gallery. Every section should answer "why should I hire this person" not "look what I can build."
Day 2Feb 2026

About Page, Projects Page — and the 'use client' Metadata Nightmare

buildfix

Built the About page and the Projects page today. The About page took the longest because I kept writing biography-style content and then deleting it. Nobody cares where you grew up. They care what you can do for them.

The Projects page has filter functionality — you can filter by Mobile, Web, Full-Stack. I built it with useState which meant the file needed 'use client' at the top.

Then I tried to add SEO metadata to it. Got this error immediately:

You are attempting to export "metadata" from a component marked with "use client", which is disallowed.

Spent about 20 minutes confused about why. The fix is that you can't have both in the same file — metadata exports only work in server components. Had to split the page into two files: a thin page.tsx server wrapper that exports the metadata and renders a Projects.tsx client component. Simple once you know it. Annoying to discover at 10pm.

// decision
Whenever a page needs both interactivity and metadata, split it. Server wrapper for metadata, client component for the interactive bits.
Day 03Mar 2026

Supabase CMS for Build Logs — Then RLS Blocked Everything

buildlaunchfix

Replaced the static build logs with a live Supabase-backed system. The idea is simple — write an entry in the admin, it shows up on the site immediately. No redeployment needed.

Built the SQL schema, the admin UI at /admin with full CRUD, and a server component page that reads from Supabase on every request. Got everything working locally. Deployed. Tried to add the first entry from the admin.

Error: new row violates row-level security policy for table "build_log_entries"

Right. RLS. The admin uses the anon key (it has to — it's a client component), and Supabase's default RLS blocks writes from the anon role. Fixed it by adding a permissive write policy in the SQL editor. The admin is already protected by a password check, so this is fine.

Then added the entries. Refreshed the build logs page. Nothing showed up. The entries were in the database — I could see them. But the page was showing the old empty state.

Turns out Next.js was caching the server component result from the first render when the table was empty. Had to add export const dynamic = 'force-dynamic' to the page file. After that it worked.

Two bugs, one session. Both fixed. Moving on.

// decision
Any page that reads from Supabase needs force-dynamic unless you specifically want caching. Default Next.js caching will bite you every time with live data.
Day 04Mar 2026

Built the Blog — Published First Article, It Didn’t Show Up

buildlaunchfix

Built a full blog system today — blog_posts table in Supabase, an admin at /admin/blog for writing posts, and a public listing page at /blog. Same pattern as the build logs but for longer-form writing.

Admin has draft/publish toggle, auto-slug generation from the title, category selector, tags, and a body field that accepts HTML. Wrote and published the first article. Went to /blog.

Empty. No posts. The "No posts yet" state was still showing.

Same issue as the build logs — forgot to add export const dynamic = 'force-dynamic'. Added it, deployed, refreshed. Article appeared.

Then clicked "Read" to read the full post. Got a 404. Turned out the individual post page (/blog/[slug]) was never built — I'd built the listing page and the admin but forgot the actual post page. Built it: fetches the post by slug, renders the HTML body, shows read time, surfaces related posts from the same category. Deployed again. Works.

Three deploys for one feature. That's how it goes sometimes.

// decision
Build the full flow before testing — listing page, post page, and admin — otherwise you’ll discover halfway through that a key piece is missing.
Day 05Mar 2026

Next.js 15 params Breaking Everything

fixbuild

Spent way too long on this one. The project case study pages (/projects/[slug]) were throwing a TypeScript error I hadn't seen before:

Type '{ params: { slug: string }; }' is not assignable to type '{ params: Promise<{ slug: string }>; }'

Turns out Next.js 15 changed how dynamic route params work. In Next.js 14 you could just do { params }: { params: { slug: string } } and destructure directly. In Next.js 15, params is now a Promise and you have to await it.

So this:

export default function Page({ params }: { params: { slug: string } })

Has to become this:

export default async function Page({ params }: { params: Promise<{ slug: string }> })

And then inside the function: const { slug } = await params;

Fixed it on both /projects/[slug] and /blog/[slug]. Annoying change to discover at runtime but at least the error message is clear enough to google.

// decision
When upgrading Next.js versions, always check the migration guide specifically for dynamic routes. The params Promise change is a breaking change that affects every dynamic page.
Day 02 · RecipesMarch 2026latest

Built complete recipe system with ingredients and steps

build

Built the complete recipe layer for FoodGram — create recipe, add ingredients, add steps, fetch complete recipe, update, and delete all working and tested.

Recipes follow the post-extension pattern decided during schema design. A recipe post is created first with post_type: "recipe", then recipe metadata is attached to that post ID. The recipe appears naturally in the feed as a post while carrying the full recipe detail separately.

The get recipe endpoint returns everything in one query — recipe details, author profile, all ingredients sorted by display order, and all steps sorted by step number. Flutter renders a complete recipe screen from a single API response with no follow-up requests needed.

Ingredients and steps use a delete-then-reinsert pattern for updates. When a user edits their ingredient list the entire list is replaced atomically. Simpler, more reliable, and eliminates the complexity of diffing what changed.

// decision
Ingredients and steps are replaced entirely on every update rather than patched individually. The complexity of tracking which items were added, removed, or reordered is not worth the marginal database efficiency gain at this scale. Full replacement is atomic, predictable, and impossible to get into an inconsistent state.
Day 06Mar 2026

Build Logs Not Showing Entries — Wrong project_id

fix

Added a new project to the admin (e-technix) and logged the first entry. The build logs page showed "1 entries logged" in the stats but the actual entry wasn't rendering anywhere on the page.

The data was in Supabase. The count was right. But the entries section was completely empty.

Took me a while to realise what was happening. The build logs page has a PROJECT_META array that defines the known projects — portfolio, zolarux, edupanion, admin. The code filters entries by matching their project_id against this array. If there's no matching entry in PROJECT_META, the project group gets filtered out entirely.

The entry I'd saved had project_id: 'e-technix' (from the admin dropdown) but PROJECT_META didn't have an entry for 'e-technix'. So it just silently disappeared.

Fixed by adding the e-technix entry to the PROJECT_META array in buildlogs_page.tsx. Entry showed up immediately.

The lesson: every time you add a new project to the admin dropdown, you also have to add it to PROJECT_META. If the IDs don't match exactly — same spelling, same case — the entries will be fetched but never displayed.

// decision
The project_id values in the admin dropdown must match the id fields in PROJECT_META exactly. Case sensitive. Consider pulling both from the same data source in the future to make this impossible to mess up.
Day 07Mar 2026

Trying to Test on Mobile — Two Hours of Network Problems

fixbuild

Wanted to test the portfolio on my phone. Should have been simple. It wasn't.

First tried the Next.js network URL (http://192.168.197.1:3000). Phone couldn't reach it even though both were on the same WiFi. Turned out the IP Next.js was showing was a virtual network adapter (from Hyper-V or something), not the actual WiFi interface. Running ipconfig showed the real IP was different. Tried that one instead. Still nothing. Windows Firewall was blocking port 3000. Added a firewall rule with netsh. Still nothing.

Tried ngrok as a fallback. Installed it, ran it, got this:

Session Status: reconnecting (failed to send authentication request: failed to fetch CRL)

The ISP or router was blocking ngrok's authentication traffic. Tried VS Code's built-in port forwarding as a third option. Got exit code 1. Needed to be signed into GitHub in VS Code, which I wasn't.

Eventually fixed it the dumb simple way — turned my phone into a hotspot, connected my laptop to the phone's hotspot, ran npm run dev again, and used the new network URL. Took 30 seconds once I stopped overthinking it.

Two hours for something that should have taken two minutes. Classic dev moment.

// decision
When testing on mobile during development, the phone hotspot method is the most reliable on Windows. Every other approach (ngrok, VS Code forwarding, LAN) has too many variables that can fail.
Day 08Mar 2026

Full SEO Pass, Sitemap, Google Search Console

buildlaunch

Did a full SEO pass today. Added Person schema markup and WebSite schema to layout.tsx — this is the structured data that tells Google who I am and links all my profiles together. The sameAs array connects LinkedIn, GitHub, X, and Zolarux. That's the thing that can eventually trigger a Knowledge Panel.

Added per-page metadata exports to every static page. Dynamic pages (/projects/[slug] and /blog/[slug]) use generateMetadata functions that pull data from Supabase or the projects data file and set title, description, and canonical URL dynamically per post/project.

Created app/sitemap.ts — Next.js auto-generates the XML file at /sitemap.xml. Submitted it to Google Search Console.

First submission came back with "Sitemap could not be read." The issue was that I submitted it before deploying. Google tried to fetch the sitemap, got nothing, flagged it. Deleted the submission, deployed, waited a few minutes, resubmitted. Went through fine.

Also created the favicon (R. with the blue dot on dark navy) and the portfolio OG thumbnail (1200×627px) for when the site gets shared on LinkedIn or WhatsApp. Both rendered from HTML using headless Chromium — gives precise typographic control that image editors can't match.

Portfolio is in a good place now. The build log entries are live, the blog is live, SEO is set up, and it's indexed on Google. Everything from here is iteration.

// decision
Always deploy before submitting a sitemap to Google. They fetch it immediately — if the URL returns nothing, the submission fails and you have to start over.​​​​​​​​​​​​​​​​
UpdateMar 2026

Added Dual Filters to Build Logs — Project + Tag

builddesign

The build logs page was getting harder to navigate as entries piled up across multiple projects. Added a dual filter system — filter by project, by tag, or combine both at the same time.

The filter bar sits between the hero and the entries. Project chips on the left (only shows projects that actually have entries), tag chips on the right — All, Build, Design, Fix, Launch, Learning. Each tag chip turns its own colour when active so you can tell at a glance what's selected.

When filters are active, a summary line appears below showing how many entries match and exactly what's filtered — something like "Showing 3 of 12 entries · Zolarux · tagged Fix." Clear button sits right next to it.

If the combination returns nothing, an empty state appears with a clear filters button rather than just a blank page.

Had the usual E-Technix id mismatch bug — the PROJECT_META array had 'e-tecnix' but the database had 'e-technix'. One missing letter meant the entire project group was silently filtered out. Fixed by matching the id to whatever was actually saved in Supabase.

The filtering all happens in memory — the server component fetches all entries once, passes them to a client component, and the filters just slice the array. No extra network calls, no loading states, instant response.

// decision
Split the page into a server wrapper (page.tsx) and a client component (BuildLogs.tsx) — same pattern as the projects page. The server wrapper handles the Supabase fetch and metadata export. The client component gets the data as props and owns all the filter state with useState.
💳
EscrowPay Engine
Fintech ledger & escrow infrastructure — Node.js & PostgreSQL double-entry accounting◐ building16 entries
Day 01Mar 2026

Designed the Wallet Home screen in Figma — first ever Figma design

builddesign

Built the Wallet Home screen from scratch in Figma — the first screen of the new Wallet UI being added to the existing Zolarux buyer protection app. This was also the first time ever opening and using Figma.

The screen includes a gradient balance card, four quick action buttons (Add Money, Withdraw, Transfer, History), a Recent Transactions section with three transaction rows showing green credits and red debits, and a bottom navigation bar with five tabs.

Design language established: primary blue #4064D7, accent orange #FFA600, background #F5F7FF, white cards with 16px border radius and subtle shadows.

// decision
Decided to add Wallet as a 5th tab in the existing bottom navigation bar rather than a separate app flow — keeping the wallet experience native to Zolarux without disrupting the existing 4-tab structure.
Day 02Mar 2026

Designed the Add Money screen with payment method selection

designbuild

Built the Add Money screen — the flow that allows users to fund their Zolarux wallet.

Includes a large ₦ 0.00 amount entry with a blue underline tap indicator, four quick preset amount buttons (₦1,000 / ₦5,000 / ₦10,000 / ₦20,000), a payment method selector with Bank Transfer (selected) and Debit Card options using radio button UI, and a Proceed to Payment CTA button at the bottom.

The ₦5,000 preset button was styled in orange #FFA600 to show the selected/active state — first integration of the accent colour into the design.

// decision
Chose to show payment method selection on the same screen as amount entry rather than a separate step — reduces friction for the most common flow (bank transfer) while still supporting card payments.
Day 03Mar 2026

Designed the Withdraw screen with bank account selector

designbuild

Built the Withdraw screen with a full bank account selector flow.

Includes an Available Balance pill at the top showing the user's withdrawable amount — label styled in orange to draw the eye to the most important number. Below it, amount entry with quick presets, a Withdraw To section showing a saved bank account (Access Bank **** 4521) with a blue selected border, and an Add New Bank Account option.

Used Figma's Ctrl+D duplicate feature for the first time to copy the quick amount buttons from the Add Money screen instead of rebuilding them — first experience with efficient Figma workflow.

// decision
Decided to show the saved bank account as pre-selected by default with a blue border — matching the pattern of how Nigerian fintech apps like Kuda and Opay handle withdrawal UX to reduce steps for repeat withdrawals.
Day 04Mar 2026

Designed the Transfer screen with live recipient confirmation UX

builddesign

Built the Transfer screen — the most UX-complex screen in the wallet flow so far.

Includes a recipient input field with a Search Contacts option, and a confirmed recipient card that appears after the user enters a phone number or wallet ID — showing the recipient's verified name, wallet ID, and a Verified checkmark. The recipient card uses an orange border (#FFA600) and orange Verified label to signal trust.

Below it, an amount entry field with quick presets and an optional note field. A Continue button leads to the review step. Chose Option B — the detailed two-step transfer flow — over Option A to build user trust before money moves.

// decision
Designed Transfer as a two-screen flow (Enter Details → Review & Confirm) instead of a single screen. This mirrors how Kuda, Opay, and real fintech apps handle transfers — the confirmation step prevents accidental sends and builds trust, which is core to the Zolarux brand.
Day 05Mar 2026

Designed the Transfer Review screen and completed full accent colour integration across all 5 screens

Built the Transfer — Review screen: a clean summary card showing all transfer details (recipient name, wallet ID, amount, transaction fee, total debit, and note) before the user confirms.

Fee shown as ₦0.00 in green to highlight zero-fee transfers. Total Debit value styled in orange (#FFA600) as the most important number on the screen. A green secured badge and a Confirm & Send CTA button sit below the card, with a red Cancel Transfer link at the bottom.

After completing this screen, did a full accent colour integration pass across all 5 screens — orange escrow badge on the balance card, orange dot on escrow transactions, orange active nav indicator, orange selected preset button, and orange available balance label — giving the design consistent visual language throughout.

// decision
Completed the accent colour integration pass as a dedicated session after all screens were structurally done rather than adding orange ad hoc during each screen. This ensured consistent usage — orange always means either "escrow-related" or "attention needed" — which aligns with how the existing Zolarux app already uses the colour.
Day 06Mar 2026

Designed Escrow Detail and Fund Escrow screens — Page 2 begins

builddesign

Started Page 2 of the Figma design file — the Escrow Screens. These are the most important screens in the entire Zolarux wallet because escrow is the core of the product.

The Escrow Detail screen shows the full status of an active escrow order. Built a 3-step progress tracker in orange showing Payment Secured → Seller Fulfils → You Confirm, an order summary card with an orange FUNDED badge, an info rows card showing Escrow ID, protection status, and a 5-day auto-release timer, and two action buttons — Confirm Delivery (blue primary) and Raise a Dispute (red outline secondary).

The Fund Escrow screen is the flow where the buyer locks funds before delivery. Includes an order summary card with an orange left accent bar, a "What happens when you fund escrow?" education card with real icons from Iconify, a wallet balance row showing green Sufficient status, an amount breakdown showing order amount, escrow fee, and total to lock, and an orange Fund Escrow CTA button — the only screen in the entire design where the primary button is orange instead of blue, intentionally signalling an escrow-specific action.

// decision
Chose to use orange as the primary CTA colour exclusively on the Fund Escrow screen — every other screen uses blue for primary actions. This creates a clear visual signal: orange means "escrow money is moving." Consistent with how orange is used throughout the design as the escrow accent colour.
Day 07Mar 2026

Designed Confirm Delivery and Raise Dispute screens — Page 2 complete

designbuild

Completed Page 2 of the Figma design file with the two most emotionally significant screens in the escrow flow.

The Confirm Delivery screen is the moment the buyer releases funds to the seller. Built a large green checkmark card as the emotional centrepiece — it feels rewarding to tap. Below it, an order summary, a "What happens after you confirm?" card showing the fund release and an irreversible action warning in red, a red warning banner, and a green Yes, I Confirm Delivery button. Green was chosen deliberately — confirming delivery is a positive, successful action and the colour reinforces that feeling.

The Raise Dispute screen handles conflict — the buyer freezes funds and triggers admin review. Built a red warning card explaining what happens, an order summary showing the frozen amount in red, four selectable dispute reason cards with radio buttons (Item not received selected by default), an additional details textarea, and a red Submit Dispute button. Every element on this screen is consistently serious in tone.

Page 2 is now complete — 4 screens covering the full escrow lifecycle: Detail → Fund → Confirm → Dispute.

// decision
Each CTA button on the escrow screens uses a different colour intentionally — orange for Fund Escrow (money being locked), green for Confirm Delivery (positive outcome, deal done), red for Submit Dispute (serious action, conflict). This creates a consistent colour language across the entire wallet: blue = standard wallet actions, orange = escrow money moving, green = success, red = danger or conflict.
Day 08Mar 2026

Designed Transaction History and Transaction Detail screens — Page 3 begins

designbuild

Started Page 3 of the Figma design file — Transaction History screens. These screens give the user a complete view of every financial movement in their wallet.

The Transaction History screen features a filter tab bar at the top with four tabs — All, Credits, Debits, and Escrow — with an active blue underline on the selected tab. Below it, a summary pill showing the total transaction count and current month, followed by transaction rows grouped by date sections (Today, Yesterday). Each row has a colour coded icon circle — green for credits, orange for escrow transactions, red for debits — making it possible to scan the list and understand each transaction type at a glance without reading the text. A Load More button sits at the bottom.

The Transaction Detail screen opens when a user taps any transaction row. It features a large hero card showing the transaction amount in green with a Completed badge, a full details card showing Type, Reference, Date and Time, Method, and Fee, and a Balance Before and After card — a fintech-grade detail that shows exactly how this transaction affected the wallet balance. A Download Receipt button sits at the bottom.

// decision
Chose to group transaction rows by date sections (Today, Yesterday, older dates) rather than a flat chronological list — this is how Kuda, Opay, and international apps like Revolut handle transaction history because it makes scanning much faster. The colour coded icon circles were kept consistent with the rest of the design — green for positive, orange for escrow, red for negative — so the colour language works across the entire app.
Day 09Mar 2026

Designed Empty State screen — Page 3 and full Wallet UI Figma design complete

designbuildlaunch

Completed Page 3 with the Empty State screen — the version of Transaction History that appears when a new user has made no transactions yet.

The screen uses the same app bar and filter tabs as the Transaction History screen for consistency. The centrepiece is a friendly empty state card with a ₦ icon in a layered blue circle, a clear "No transactions yet" heading, and an explanation telling the user exactly what to do to get started.

Below the text, three quick action suggestion chips give the user immediate next steps — Add Money and Transfer in blue, Fund Escrow in orange — keeping the colour language consistent with the rest of the design. A strong Add Money to Get Started CTA button sits at the bottom.

With this screen, Page 3 is complete and the entire Wallet UI Figma design is done — 12 screens across 3 pages covering every wallet flow: home, add money, withdraw, transfer, escrow lifecycle, transaction history, and empty states.

// decision
Designed the empty state as an opportunity to guide the user into action rather than just showing a blank screen. The three suggestion chips — Add Money, Transfer, Fund Escrow — represent the three most common first actions a new user would take. Fund Escrow was given the orange accent colour to reinforce that escrow is the core differentiator of Zolarux, even on an empty state screen.
Day 10April 2026

Project initialized — Node.js setup, folder structure, Supabase connected

buildlearning

Started building EscrowPay Engine from scratch — a standalone fintech ledger and escrow infrastructure layer built with Node.js and PostgreSQL. This is not an app. It is an engine that any platform can consume via REST API.

Initialized the Node.js project, installed all core dependencies — Express, pg, dotenv, helmet, cors, uuid, express-rate-limit — and created the full production-grade folder structure: controllers, services, repositories, domain, middleware, routes, utils, plus database/migrations, database/seeds, and tests directories.

Configured environment variables, created the database connection file using pg Pool with SSL support, and built the Express server with a health check endpoint. After resolving a network issue where Supabase's port 6543 was blocked by the Nigerian ISP, connected to Supabase PostgreSQL using port 5432 on the session pooler. Server confirmed running on port 3000 with database connected.

// decision
Chose pg (node-postgres) over Supabase's JavaScript client for the database connection — EscrowPay is a backend engine that needs raw SQL control, not an ORM or abstraction layer. Direct SQL gives full control over transactions, atomicity, and double-entry accounting logic.
Day 11Apr 2026

All 5 database tables migrated — the schema that most fintech systems get wrong

build

Wrote and ran all 5 migration files against the Supabase PostgreSQL database — establishing the foundation that every engine in the system is built on top of.

The 5 tables: accounts (wallet identity — no balance column), transactions (every financial operation), ledger_entries (the DR/CR source of truth), escrow_orders (escrow lifecycle per order), and idempotency_keys (prevents duplicate operations on retried requests).

Two critical design decisions enforced at the schema level: no balance column on accounts — balance is always computed from ledger entries — and all monetary amounts stored as BIGINT in kobo, never DECIMAL or FLOAT. These are the two mistakes that break most fintech ledger systems and both are avoided by design, not by convention.

// decision
Enforced the no-stored-balance rule and integer-only money rule at the database level using CHECK constraints — not just in application code. If application logic ever breaks, the database itself rejects invalid data. Financial invariants belong at the lowest possible layer.
Day 12Apr 2026

Ledger Engine and Transaction Engine built — double-entry accounting is live

build

Built the two most critical engines in the system — the Ledger Engine and the Transaction Engine.

The Ledger Engine handles double-entry accounting. Every financial movement creates exactly two ledger entries — one DR and one CR that must always balance. Balance is computed by summing ledger history: SUM(CR) - SUM(DR). There is no balance column anywhere in the system. The engine also includes a global ledger health check that verifies total DR equals total CR across all entries.

The Transaction Engine wraps every money movement in a database-level atomic operation using BEGIN / COMMIT / ROLLBACK. If anything fails mid-operation, everything rolls back — no partial state is ever possible. It also validates accounts exist and are active, enforces currency matching, and checks sufficient balance before any debit.

Also built the Account Repository — the database access layer for accounts — keeping SQL queries completely separate from business logic.

// decision
The Transaction Engine accepts a database client as a parameter rather than creating its own connection — this allows the Escrow Engine to pass an existing transaction client so multiple operations can be wrapped in a single atomic database transaction. This is the correct architecture for a financial system where escrow involves multiple coordinated money movements.
Day 13Apr 2026

Wallet Service and REST API live — deposit, withdraw, transfer all tested

buildlaunch

Built the Wallet Service — the user-facing layer that orchestrates wallet operations using the Ledger and Transaction engines underneath. Operations: create_wallet, deposit, withdraw, transfer, get_balance.

Created the full wallet REST API with controllers and routes. Seeded a system account — a special ledger account that acts as the source for deposits and the sink for withdrawals, representing money arriving from and leaving to external payment providers like Paystack.

Tested all 4 operations end to end using Thunder Client: deposited ₦5,000 into a wallet, withdrew ₦1,000, transferred ₦2,000 to a second wallet, and verified the recipient received exactly ₦2,000. The math was correct at every step. Every operation created paired DR/CR ledger entries with immutable timestamps.

// decision
Deposits and withdrawals go through the system account rather than creating unbalanced ledger entries — this keeps the double-entry invariant intact even for external money movements. In production, the system account is credited when Paystack confirms a payment via webhook, and debited when a bank transfer is initiated. The ledger always balances.
Day 14Apr 2026

Escrow Engine built and tested — the full buyer protection lifecycle is live

buildlaunch

Built the Escrow Engine — the most important component in the entire system and the core of what makes Zolarux possible.

The escrow model is correct by design: each escrow order gets its own dedicated ledger account (type: escrow_wallet) — not a bank account, just a row in the accounts table. Money flows from buyer wallet → escrow account → seller wallet. The escrow account balance becomes exactly zero after release.

Tested the complete lifecycle end to end: created an escrow order for an iPhone 15 Pro Max at ₦1,500, funded it from the buyer wallet (DR buyer / CR escrow), confirmed delivery to release funds to the seller (DR escrow / CR seller). Verified the escrow account balance returned to ₦0.00 after release — every kobo accounted for. Also built dispute and refund flows.

Built the full escrow REST API: POST /escrow/create, POST /escrow/fund, POST /escrow/release, POST /escrow/refund, POST /escrow/dispute, GET /escrow/:id.

// decision
One dedicated escrow account per order — not one shared escrow pool. This means every order's money is fully isolated and instantly auditable with a single query: SELECT * FROM ledger_entries WHERE account_id = 'escrow_order_xxx'. This is how large financial systems isolate funds and it makes disputes, refunds, and reconciliation trivial.
Day 15Apr 2026

Multi-tenant platform layer built — EscrowPay becomes escrow-as-a-service for Africa

buildlaunch

Made a critical architectural decision: EscrowPay is not just Zolarux's backend. It is a multi-tenant escrow infrastructure engine — the first Nigerian-built escrow-as-a-service platform. Any marketplace, freelance platform, rental platform, or B2B trade platform can plug into it via API.

Built the Platform Service and added two new migration tables — platforms (stores registered platforms with their API keys and prefixes) and updated accounts to include platform_id and wallet_id columns.

EscrowPay now generates platform-prefixed wallet IDs — ZLX-685385-3442 for Zolarux users, FLW-441782-7823 for a freelance platform, RNT-229018-4401 for a rental platform. No ID collisions across platforms ever. Registered Zolarux as the first platform and generated its first wallet: ZLX-685385-3442.

The architecture mirrors how Google owns Android but Android powers other phones — EscrowPay owns the infrastructure, Zolarux is the first consumer, other platforms follow.

// decision
Platform prefixes are exactly 3 letters and enforced as UNIQUE in the database — this is permanent. Once ZLX is Zolarux's prefix, no other platform can take it. The wallet ID format (PREFIX-XXXXXX-XXXX) is collision-resistant by design — 6-digit + 4-digit random segments give 9 billion possible IDs per platform prefix.
Day 16Apr 2026latest

Phase 4 complete — API key auth, rate limiting, validation, ledger health all live

buildlaunch

Completed Phase 4 — production hardening. The engine is now secured, rate limited, and validated end to end.

Built API key authentication middleware — every request to wallet, escrow, and transaction routes must include a valid platform API key in the x-api-key header. The middleware identifies the platform and attaches its context to every request. Invalid or missing keys are rejected with 401.

Built three-tier rate limiting — 100 requests/minute for general API calls, 20 requests/minute for financial operations (deposit, withdraw, transfer, escrow fund/release), and 5 requests/hour for platform registration. Built input validation middleware with amount type checking, UUID validation, currency validation, and a maximum single transaction limit of ₦10,000,000.

Built and tested the transaction history and ledger health endpoints. The ledger health check confirms DR = CR globally across all entries — total debits: ₦10,011,000, total credits: ₦10,011,000, difference: 0. All 7 financial invariants are enforced and passing.

// decision
Three separate rate limit tiers instead of one global limit — financial operations get the strictest limit (20/min) because they move real money and abuse is the most damaging there. Platform registration gets the strictest of all (5/hour) because it creates permanent API credentials. General read endpoints get the most generous limit. Security should be proportional to risk.
🎓
E-Technix
Digital & tech skills training platform — Nigeria & UK◐ building2 entries
Day 01March 2026

Project Kickoff — Branding, Homepage & Setup

builddesign

Kicked off the E-Technix training platform — a joint venture with my UK-based director. The goal is to train people in Nigeria and the UK on real, in-demand digital and tech skills across 6 specialisation tracks in a structured 6–9 month programme.

Today I defined the full programme structure, created the branding from scratch (cyan + orange + deep navy, Syne + Figtree fonts), designed and built the complete homepage prototype in HTML/CSS, scaffolded the Next.js 16 project with TypeScript and Tailwind, and pushed the first commit to GitHub. The site will be deployed on Vercel and linked to the custom domain e-technix.com via Hostinger DNS.

Bug 1 — npm install appeared stuck for 30+ minutes. Cause: slow network throttling npm downloads. Fix: killed the process, cleared cache with npm cache clean --force, re-ran the command. The first install had actually completed silently.

Bug 2 — Turbopack not supported on win32/x64. Native bindings failed to load. Fix: switched to Webpack with npm run dev -- --webpack.

Bug 3 — React hydration mismatch warning in dev. Cause: Grammarly browser extension injecting HTML attributes before React hydrates. Not a real bug — safe to ignore.

// decision
Chose Next.js + Vercel over Hostinger hosting because the current Hostinger plan doesn't support Node.js. Domain will stay on Hostinger and point to Vercel via DNS records. Can migrate later if we upgrade the plan.
Day 02March 2026latest

All Pages Built & Site Deployed Live

builddesignlaunch

Converted the full HTML prototype into a proper Next.js 16 project with TypeScript and Tailwind. Built all pages and components from scratch — everything is now live on e-technix.com via Vercel.

Pages built:

Home page (Hero, Stats, How It Works, Track Grid, Foundation, Certifications, CTA), Programs page with full breakdown of all 6 specialisation tracks, How It Works page with all 4 phases in detail, Certifications page with all 6 certificates, and the Register page with Paystack payment integration.

Components created:

Navbar.tsx, Footer.tsx, Hero.tsx, sections.tsx (Stats, HowItWorks, TrackGrid, Foundation, Certifications, CTASection). All track and programme data lives in a single src/lib/data/tracks.ts file as the source of truth.

Payment integration:

Integrated Paystack using their inline JS approach instead of the react-paystack npm package, which doesn't support React 19. The script loads dynamically on the register page, opens a native Paystack popup on payment, and redirects to WhatsApp with the student's full details pre-filled after a successful transaction.

Deployment:

Project pushed to GitHub and deployed on Vercel. Custom domain e-technix.com connected via Hostinger nameservers pointing to Vercel. Cloudflare proxy was detected — boss has been notified to disable it (grey cloud in Cloudflare DNS settings).

Bugs encountered:

Bug 4 — sections.tsx and Footer.tsx were missing 'use client' at the top despite using onMouseEnter and onMouseLeave event handlers. Next.js threw a server/client boundary error. Fix: added 'use client' as the first line of both files.

Bug 5 — react-paystack@6.0.0 does not support React 19. npm install failed with a peer dependency conflict. Fix: dropped the package entirely and used Paystack's native inline JS script loaded dynamically via useEffect. No npm package needed.

// decision
1. Used Paystack's native inline JS instead of react-paystack — the npm package doesn't support React 19 and --force risked runtime crashes. 2. Kept all track and programme data in a single tracks.ts file rather than hardcoding it across pages — makes future updates (pricing, new tracks, tools) a one-file change. 3. Used inline styles throughout components instead of Tailwind utility classes — gives full control over the dark theme CSS variables (--cyan, --surface, --border etc.) that Tailwind can't reference directly without extra config. 4. Deployed on Vercel instead of Hostinger — current Hostinger plan doesn't support Node.js. Domain stays on Hostinger and points to Vercel via nameservers. Can migrate later when the plan is upgraded. 5. Used Paystack's test keys in .env.local during development — will swap to live keys after Paystack account verification is complete. 6. Priced tracks individually based on complexity rather than a flat fee — AI & Agentic Systems at the top (₦280,000) and Business Development at the most accessible (₦150,000), with instalment options at a small premium over half to account for the payment gap.
🏛️
ICAN Warri District
Regional website for ICAN Warri◐ building1 entries
Day 01 · FoundationMar 2026latest

Project setup, architecture planning and base layout

designbuildlearning
Started building the official website for ICAN Warri District Society — a professional accounting body site with a public-facing side and a members portal.

Before writing a single line of code, did a full architectural review of 4 existing ICAN district websites (Ibadan, Ikeja, Lekki, Abuja) to understand the portal system and content structure. Discovered all four districts run the same PHP portal system deployed independently — not a central ICAN server.

Stack chosen: Next.js (App Router) + TypeScript + Tailwind CSS, deployed on Vercel.

What was built:
— Scaffolded the Next.js project with TypeScript and Tailwind
— Updated page metadata (title + description) for SEO
— Built the Navbar component with navigation links and a Member Login CTA
— Built the Footer component with quick links, contact info and copyright
— Wired both into layout.tsx so they appear automatically on every page without repeating code
— Produced a full architecture PDF mapping all 4 zones: public site, member portal, admin panel, and backend

Learning milestone: This is the first Next.js + TypeScript project. Completed a full crash course covering JavaScript fundamentals, TypeScript, React (components, state, props), and Next.js (routing, server vs client components, data fetching) before touching any code.
// decision
Decided to build the public website first and leave the member portal as TBD until the client confirms what features they need inside the dashboard. This avoids building on assumptions and keeps the project unblocked.
🍔
FoodGram
Food social network — Node.js, PostgreSQL & Flutter◐ building11 entries
Day 01 · FoundationMarch 2026

Built and deployed a production Node.js API from scratch

buildlearning

Designed and built the complete backend foundation for FoodGram — a food social network. Started from zero: no boilerplate, no Supabase, no shortcuts.

Stack: Node.js + Express, PostgreSQL, Railway for hosting. Structured the entire project architecture myself — config, middleware, routes, controllers, services, and database layers all separated by responsibility.

Configured a PostgreSQL connection pool with error handling, a global error handler that catches and classifies every failure type, security middleware (Helmet, CORS, rate awareness), and environment validation that refuses to start the server if any required variable is missing.

Deployed to Railway with environment variables secured, auto-deploy on every GitHub push configured, and a live health check endpoint responding in production.

// decision
Chose to build a fully custom backend instead of using Supabase — deliberately taking the harder path to own and understand every layer of the system. Chose Railway over Oracle Cloud to avoid infrastructure complexity at this stage, with a clear migration path later when scale demands it.
Day 02 · AuthenticationMarch 2026

Built complete authentication system with JWT and bcrypt

buildlearning

Built the entire authentication layer for FoodGram from scratch — register, login, and protected routes all working and tested.

What was built: A fully layered auth system following the route → controller → service → database architecture. Each layer has a single responsibility. Routes define URLs, controllers validate input, services handle business logic, middleware protects routes.

Passwords are hashed with bcrypt at cost factor 12 before hitting the database. Raw passwords never stored, never returned in responses. JWT tokens are signed with a secret key, carry the user ID and username, and expire in 7 days.

The authenticate middleware intercepts every protected request, verifies the token, and attaches the user ID to the request object before the controller ever runs. Invalid or missing tokens are blocked with a 401 before reaching any business logic.

All endpoints tested with Thunder Client — register, login, protected route with token, and protected route without token all behaving exactly as designed.

// decision
Kept the error message for failed login intentionally vague — "Invalid email or password" rather than specifying which field is wrong. This prevents attackers from using the login endpoint to enumerate which emails are registered on the platform. Security over user convenience at this layer.
Day 02 · PostsMarch 2026

Built complete posts system with feed, likes and comments

build

Built the entire posts layer for FoodGram — create post, fetch feed, get post by ID, like, unlike, add comment, get comments, and delete post all working and tested.

The feed query is the most complex piece — a single SQL query that joins posts with users, aggregates all media into a JSON array per post, and checks whether the current user has liked or saved each post. One database round trip for the entire feed regardless of how many posts are returned.

Posts support multiple content types — regular, recipe, and menu item — with cuisine tagging, location, and optional sale price built in from day one. Media uploads are stubbed and ready for Cloudflare R2 integration in the next phase.

All endpoints protected by JWT auth middleware. Tested with Thunder Client — create, like, comment, and fetch all returning correct data with counters updating in real time.

// decision
Posts are never hard deleted — they are archived. Setting is_archived = TRUE hides them from feeds and profiles while preserving the row in the database. This protects referential integrity since likes, comments, and future orders all reference post IDs. Deleting a post with active references would either cascade delete important data or leave orphaned records.
Day 02 · Social GraphMarch 2026

Built user profiles, follow system and social feed

build

Built the complete user and social graph layer for FoodGram — profile lookup, profile editing, follow, unfollow, followers list, following list, and user posts all working and tested end to end.

The follow system handles private accounts natively — following a private account creates a pending request instead of an immediate follow. The feed query already filters by accepted follows so pending requests never leak content through.

Verified the core social loop end to end — registered two users, Rex followed Temi, Temi posted, and Temi's post appeared correctly in Rex's feed with the right username, display name, and interaction state attached. Empty feed before the follow, populated feed after. Exactly as designed.

Profile responses include is_own_profile, is_following, and follow_requested flags so Flutter can render the correct UI state — edit button vs follow button vs pending button — from a single API response.

// decision
The feed intentionally excludes your own posts. You follow people to see their content — your own posts are accessible on your profile. This keeps the feed focused on discovery and social content rather than becoming a mirror of your own activity.
Day 02 · StoriesMarch 2026

Built stories system with 24 hour expiry and view tracking

build

Built the complete stories layer for FoodGram — create story, feed stories grouped by user, my stories with view counts, record a view, get story viewers, and delete story all working and tested.

Stories expire automatically after 24 hours using two mechanisms working together. Every read query filters by expires_at > NOW() so expired stories are invisible immediately without waiting for cleanup. A background job then runs every 30 minutes and hard deletes the expired rows to keep the database clean.

The feed groups stories by user — the same pattern Instagram uses. One bubble per user in the horizontal story tray, with an has_unviewed flag so Flutter knows whether to show the colored ring or the greyed out ring around each bubble.

Added a database keepalive ping every 4 minutes to prevent Railway's free tier database from going idle between requests during development. Connection timeout also increased from 2 to 10 seconds for resilience.

// decision
Story expiry uses two layers intentionally — query-level filtering plus a background deletion job. The query filter makes expiry instant and reliable. The deletion job keeps the database clean and prevents unbounded table growth. Either alone would work but both together is the correct production pattern.
Day 02 · RecipesMarch 2026

Built complete recipe system with ingredients and steps

build

Built the complete recipe layer for FoodGram — create recipe, add ingredients, add steps, fetch complete recipe, update, and delete all working and tested.

Recipes follow the post-extension pattern decided during schema design. A recipe post is created first with post_type: "recipe", then recipe metadata is attached to that post ID. The recipe appears naturally in the feed as a post while carrying the full recipe detail separately.

The get recipe endpoint returns everything in one query — recipe details, author profile, all ingredients sorted by display order, and all steps sorted by step number. Flutter renders a complete recipe screen from a single API response with no follow-up requests needed.

Ingredients and steps use a delete-then-reinsert pattern for updates. When a user edits their ingredient list the entire list is replaced atomically. Simpler, more reliable, and eliminates the complexity of diffing what changed.

// decision
Ingredients and steps are replaced entirely on every update rather than patched individually. The complexity of tracking which items were added, removed, or reordered is not worth the marginal database efficiency gain at this scale. Full replacement is atomic, predictable, and impossible to get into an inconsistent state.
Day 02 · CommerceMarch 2026

Built complete commerce system — business accounts and food ordering

buildlaunch

Built the complete commerce layer for FoodGram — business account upgrades, food ordering directly from posts, order status lifecycle, and cancellation all working and tested end to end.

The full order lifecycle was verified — Temi placed an order for jollof rice from Rex's Kitchen, Rex confirmed and delivered it, and Temi's order history showed the complete record with precise timestamps for every status transition.

Price is always read from the post, never from the client request. A buyer cannot manipulate the price by sending a different value — the server pulls the price directly from the database. This is a fundamental commerce security principle.

Both buyers and sellers can cancel orders with an optional cancellation reason. Delivered orders cannot be cancelled. Sellers control the order status advancement — buyers cannot mark their own orders as delivered.

// decision
Commerce is built directly into the social feed rather than as a separate marketplace. A post can be marked for sale from the same create post flow. This keeps the product cohesive — discovery and commerce happen in the same scroll. The separation of business profile from user account means anyone can upgrade without losing their social identity or follower count.
Day 03 · Flutter AuthMarch 2026

Built Flutter auth flow — splash, login and register screens working on device

build

Built the complete Flutter authentication flow for FoodGram — splash screen, login, and register all working on a real Android device connecting to the live Railway backend.

The splash screen checks secure storage for an existing JWT token on app launch. Logged in users go straight to the feed. New users go to login. Animated logo fades and scales in over 1.2 seconds on a warm orange background.

The auth provider manages all authentication state using Riverpod's NotifierProvider. Login and register both call the live API, store the JWT token in Android's encrypted keychain via flutter_secure_storage, and navigate to the feed on success.

Error handling reads the actual error message from Dio's response body rather than parsing raw exception strings — so users see clean messages like "This username is already taken" instead of generic fallbacks.

// decision
JWT tokens are stored in flutter_secure_storage which uses Android Keystore and iOS Keychain — the device's native encrypted storage. Never SharedPreferences for tokens. The token is attached to every API request automatically via a Dio interceptor so no screen ever manually handles authentication headers.
Day 03 · FeedMarch 2026

Built the FoodGram feed — posts loading live from backend on real device

build

Built the complete feed screen for FoodGram — posts loading live from the Railway backend, rendered on a real Android device with infinite scroll, pull to refresh, optimistic likes, and empty state handling.

The feed provider manages all feed state using Riverpod's NotifierProvider — initial load, pagination, load more, and optimistic like updates all handled in one place. The UI rebuilds automatically when state changes.

The post card renders author avatar with initials fallback, location, cuisine tag, media, like and comment counts, price badge for food items, and relative timestamps. Verified badges and recipe/order type indicators built in.

Optimistic likes update the UI instantly on tap — the heart fills and the count changes immediately while the API call happens in the background. If the API fails the update reverts. Zero perceived latency.

Verified the full social loop on device — logged in as Rex, feed showed Temi's suya post correctly. Empty feed state shows for new users with no follows.

// decision
The story tray is rendered as a placeholder for now with dummy data. Real story data will be wired in once the story provider is built. Building the layout and scroll behavior first, then connecting real data, is faster than trying to build both simultaneously.
Day 04 · Stories & OrdersApril 2026

Built story viewer and orders screen — full social and commerce loop on device

build

Built the story viewer and orders screen, completing the core feature set of FoodGram on device.

The story viewer shows stories in chronological order with progress bars per story, tap left/right navigation, long press to pause, and swipe down to dismiss. Stories auto-advance after 5 seconds and exit when all stories in a group are viewed.

The story tray in the feed is now wired to live data — story bubbles show with colored rings for unviewed stories and grey rings for viewed ones. Tapping a bubble opens the full screen viewer for that user's stories.

The orders screen has two tabs — My Orders for buyers and Incoming for sellers. Rex can see Temi's delivered order in the incoming tab with full order details, delivery address, notes, and amount. Sellers can advance order status through the full lifecycle directly from the screen.

// decision
Story ordering was fixed to show oldest first — same as Instagram and WhatsApp. Users should experience stories in the order they were posted, not newest first. Viewed stories stay visible behind unviewed ones so users can always go back.
Day 05 · Media UploadApril 2026latest

Real photo uploads working — Cloudflare R2 fully integrated

build

Integrated Cloudflare R2 media storage into FoodGram — users can now upload real photos when creating posts. The full pipeline is working: photo picked from gallery on device, uploaded to Node.js backend, stored in Cloudflare R2, public URL saved to PostgreSQL, image renders in the feed and profile grid.

Built a dedicated media upload service on the backend with file type validation, size limits (10MB images, 100MB videos), UUID-based filenames organized into folders by type (posts/, stories/, avatars/), and public URL generation via Cloudflare R2's public bucket access.

On the Flutter side, used Dio multipart form upload with a dedicated media client set to 60 second timeouts — necessary because image uploads take longer than regular API calls. The standard 10 second timeout was causing connection failures.

Photos now show in the profile grid and feed as real images instead of caption text placeholders.

// decision
Media uploads go through the backend rather than directly from Flutter to R2. This keeps R2 credentials secure on the server — Flutter never sees the access keys. The backend validates file type and size before accepting the upload, preventing abuse.

Following the build?

If you are a founder or engineer interested in working together, reach out.