
Nick Ayoola
FULL-STACK SOFTWARE ENGINEER - REACT / REACT NATIVE
Full-stack Software Engineer with 12+ years' experience building production web platforms, including 9 years on a central engineering team at a global data and analytics company. The past two years, alongside that role, I've been designing and shipping products solo, culminating in Q4U: a React/React Native marketplace app built end to end, from architecture through App Store/Play Store release to live operations.
Q4U: Skip the queue
React Native / Expo / TypeScript / Supabase / Stripe (live on iOS & Android)
Two-sided marketplace app enabling users to buy and sell queue-holding slots. Built end to end: Stripe Connect escrow with manual-capture holds and automated payouts, realtime job coordination via Supabase channels, QR-code handoff verification, Twilio phone verification, push notifications, Sentry monitoring, and over-the-air updates. Covers the full lifecycle from architecture and build through store submission and live operations.
Automated Car Lead Dashboard
React / Next.js / .NET / PostgreSQL
Automates lead capture from Autotrader listings using a .NET scraper and a Python license-plate recognition model, syncing results to Google Sheets. Deployed and operated on Azure.Delivered as a freelance project and continues to receive new feature work for the client.
Q4U: A two-sided queue marketplace, built solo with React Native & Expo
Q4U lets people pay a nearby "queuer" to physically hold their place in a line, and pays that queuer out automatically when the handoff is done. A complete two-sided marketplace with real payments, identity verification, and live location, built from scratch for iOS and Android from a single codebase.
Overview
Waiting in line is one of the last chores you can't outsource. Q4U fixes that: Requesters post a paid request to have their place held; nearby Queuers accept the job, hold the spot, and hand it over in person, verified by a QR code, with payment captured and paid out automatically on completion.I designed and built the entire product end to end: the mobile app, the backend, the payments and payout rails, identity verification, and the full store submission, and I now run it in production.
My role
Sole developer & product owner: product design, UI, React Native app, Supabase backend & database, Stripe payments + payouts, phone & identity verification, push notifications, observability, and iOS/Android release engineering with EAS.
Tech stack
Mobile: React Native 0.81 (New Architecture), Expo SDK 54, Expo Router, TypeScript
Backend: Supabase, Postgres with Row-Level Security, Deno Edge Functions, Realtime, Storage
Payments: Stripe Payments (manual-capture escrow) + Stripe Connect (Express payouts)
Verification: Twilio Verify (SMS phone verification) + manual ID/selfie review
Ops & delivery: EAS Build, EAS Update (over-the-air updates), Sentry (error + performance monitoring)
Other: Zustand, react-native-maps, expo-location, expo-camera, QR codes, push notifications, Resend (email)
Overview
1. Escrow-style payments and automated payoutsMoney is the hard part of any marketplace. I used Stripe manual-capture payment intents to hold a requester's funds when they post a job, then capture on completion, an escrow-like flow that protects both sides. On handoff, an Edge Function captures the payment and uses Stripe Connect (separate charges & transfers) to pay the queuer's share, keeping the platform fee, then Stripe pays out to their bank automatically.The subtle part was the onboarding return flow: Stripe only accepts http(s) return URLs and rejects custom app schemes. I solved it with an https redirect Edge Function that 302s back into the app via a deep link, plus an Expo Router +native-intent rewrite so the user lands exactly where they left off.Outcome: a full hold → capture → transfer → payout pipeline running on live Stripe (validated end-to-end in sandbox first).2. Debugging an auth deadlock that froze the whole appScreens would spin forever and user data wouldn't load after re-login. The cause was subtle: awaiting a Supabase query inside the onAuthStateChange callback held the auth client's lock and deadlocked every subsequent request. Deferring the profile fetch out of the callback fixed it instantly.Outcome: a class of "random freezes" eliminated, the kind of bug that only gets solved by understanding why, not just retrying.3. Native platform quirks: keyboards & builds
Edge-to-edge keyboards: Android's edge-to-edge mode breaks the standard
KeyboardAvoidingView, so inputs sat under the keyboard. I moved the app toreact-native-keyboard-controllerfor correct, app-wide behaviour on both OSes.A Hermes build crash: Supabase's ESM build does a dynamic
import()of OpenTelemetry that the Hermes engine can't compile. I fixed it with a MetroresolveRequestredirect to the CommonJS build, a one-line fix that took real digging to find.
Outcome: clean builds and a polished, native-feeling UX on both platforms.4. Trust & safety on a two-sided marketplaceStrangers meet in person and money changes hands, so trust matters. I built:
QR-verified handoffs, the queuer shows a code, the requester scans it, and an Edge Function verifies it before releasing payment.
Row-Level Security across every table, with privileged operations locked behind service-role Edge Functions.
Phone verification via Twilio Verify, and identity verification. When a third-party provider stalled before launch, I shipped a pragmatic manual flow (camera-only ID + selfie to a private bucket, admin approval) so verification wasn't a launch blocker. That was a product call, not just a code one.
5. Offline resilience, and knowing where to stopOpening the app with no connection showed a long black screen and a blank profile. The root cause was startup network calls that hung with no timeout. I fixed it in layers: a global fetch timeout so offline calls fail fast, a branded loading state instead of a blank screen, a persisted profile so the name renders instantly from cache on cold start, and recoverable "can't load" states.The decision I'm most pleased with is where I stopped: I deliberately did not go full offline-first. In a realtime payments marketplace, stale data actively misleads (a job cached as "active" might really be completed), so the rule is cache for display continuity, always revalidate, and never let a user take a money action on stale data.Outcome: a fast, legible cold start offline, and a defensible boundary on how far to take caching.6. Shipping and operating in productionBuilding it was half the job; taking it live and keeping it healthy was the other.
Dual-store release with EAS: privacy nutrition labels, Play Data safety, age ratings, demo review accounts, and gated test-mode payments so store reviewers could exercise the full paid flow against the live app without a real charge.
Observability with Sentry: after launch it flagged a fatal crash on notification-tap, caused by a reused Supabase realtime channel. I diagnosed it from the stack trace alone and shipped the fix as an over-the-air update, no rebuild, no store round-trip.
Outcome: shipped solo to both stores, and the ability to diagnose and fix a production issue in minutes rather than a release cycle.
What I shipped
Two user modes (Requester / Queuer) in one app
Live map of nearby requests with custom pins
Full job lifecycle: post → accept → near-front → QR handoff → complete
Real card payments with escrow-style holds + saved cards (PCI-safe)
Automated Stripe Connect payouts with just-in-time onboarding
Phone verification (Twilio) + manual identity verification
Two-way reviews & ratings
Push + in-app + email notifications
Offline resilience: bounded network, cached profile, graceful retry states
Monitored with Sentry; shipped and hotfixed via EAS over-the-air updates
~20 screens, iOS + Android from one codebase
What I shipped
Building a marketplace solo meant owning every layer, from a Postgres RLS policy to a Stripe webhook to an iOS provisioning profile to a live-crash hotfix. The biggest lessons were in the unglamorous parts: payments edge cases, auth races, native platform differences, and the store-submission and operations work that turns a build into a shipped product. I lean on modern tooling, including AI pair-programming, to move quickly, but every architecture decision, integration, and bug fix went through my own understanding and testing. That's how I shipped it solo to both app stores and kept it running in production.Status: Live on iOS, available on the App Store. Android release pending Play Store review.
Available for React Native / Expo freelance work.
Nick Ayoola · Email · LinkedIn
Turning a daily hunt through car adverts into an automated sourcing pipeline
Built with Next.js, .NET & a Python OCR microservice on Azure
Position One Motor Company used to source vehicles by hand every morning: trawling AutoTrader, copying advert details, looking up each registration in a second system for history and valuation, then re-keying everything into a Google Sheet. I replaced the whole routine with a three-part system that scrapes the listings, reads number plates from vehicle photos, enriches each record from external services, stores it in PostgreSQL, and keeps their Google Sheet up to date on its own, every hour.Status: Live in production. Runs an automated hourly pipeline for the client, powered up and down on a schedule to keep cloud costs near zero. Designed and built solo.

Built with Next.js, .NET & a Python OCR microservice on Azure
Position One Motor Company used to source vehicles by hand every morning: trawling AutoTrader, copying advert details, looking up each registration in a second system for history and valuation, then re-keying everything into a Google Sheet. I replaced the whole routine with a three-part system that scrapes the listings, reads number plates from vehicle photos, enriches each record from external services, stores it in PostgreSQL, and keeps their Google Sheet up to date on its own, every hour.Status: Live in production. Runs an automated hourly pipeline for the client, powered up and down on a schedule to keep cloud costs near zero. Designed and built solo.
My role
Sole developer & product owner: architecture, the Next.js dashboard, the .NET backend and data model, the Python OCR microservice, PostgreSQL/EF Core, Google Sheets integration, JWT authentication, and the full Azure deployment, automation, and cost engineering.
My role
Sole developer & product owner: architecture, the Next.js dashboard, the .NET backend and data model, the Python OCR microservice, PostgreSQL/EF Core, Google Sheets integration, JWT authentication, and the full Azure deployment, automation, and cost engineering.
Tech stack
Frontend: Next.js 15 (App Router, standalone), React 19, TypeScript, Tailwind v4, NextAuth v5 (JWT sessions), SWR
Backend: .NET 8 Web API, EF Core (LINQ + migrations), PostgreSQL via Npgsql (Neon), JWT bearer auth, BCrypt
Microservice: Python, FastAPI, YOLOv8 (Ultralytics) for plate detection, PaddleOCR for text, OpenCV
Data & integrations: PostgreSQL, Google Sheets API, AutoTrader listing data, external vehicle history & valuation APIs
Cloud & delivery: Azure App Service (frontend + backend), Azure Container Apps (scale-to-zero OCR), Azure Container Registry, Azure Logic Apps (Recurrence triggers + Managed Identity), Docker
Ops: hourly scrape automation, morning/evening server start-stop, dry-run import mode, marker-based incremental processing, container-log monitoring
Key challenges
1. On-demand computer vision without paying for idle computeThe vision model is the heaviest thing in the stack, yet it only needs to run for a few minutes an hour. Keeping a GPU-class container warm all day would have dominated the running cost. I ran the OCR service on Azure Container Apps scaled to zero, and drove it from an Azure Logic App that, every hour between 9am and 7pm, starts the container, waits for it to warm, triggers the scrape, waits for the run to finish, then shuts it back down. The start/stop calls hit Azure's management API using a Managed Identity, so the most privileged part of the system holds no stored secrets.
Outcome: full computer-vision enrichment on demand, with the OCR container billing essentially nothing between runs.2. Reading the plates the model gets wrong, rightNumber plates in photos are small, angled, and dirty, and raw OCR routinely confuses O/0 and I/L/1. I built a two-stage reader: YOLOv8 locates and crops the plate, then PaddleOCR reads it, then a correction pass uses the fixed UK plate layout to disambiguate each character by position rather than guessing. Where a reading is still ambiguous, I expand the plausible candidates and validate them against the downstream lookup, taking the first that resolves to a real registered vehicle.
Outcome: near-miss reads like "L1 OO1" get turned back into valid registrations instead of dead lookups, so the enrichment step actually fires.3. A data-import pipeline that's safe to run, safe to rerun, and testableThe endpoint that imports car records into the pipeline was risky to run blindly, it could reprocess the whole dataset or write bad data, and I needed to be able to exercise it without side effects. I built three habits into it. Input validation with clear error responses, so bad requests fail fast and legibly. A dry-run mode that reports exactly what a run would do without writing anything, which I drove through Postman to verify behaviour safely. And incremental processing via a marker/cursor: the run records how far it got, so a rerun only picks up records past that marker instead of starting over, on top of the scrape itself stopping at the first advert it has already stored.
Outcome: an idempotent, incremental import, no full reprocessing, no accidental writes during testing, and clear failures on bad input.4. The boundary between my code and the databaseEF Core turns LINQ into SQL and only supports a subset of .NET methods; get it wrong and a query either throws or silently drags rows into memory to evaluate client-side. Two bugs lived right on that seam:
A LINQ translation failure: a query blew up at runtime with EF Core's "could not be translated" error. The culprit was string.CompareOrdinal, which EF Core can't turn into SQL. I switched it to .CompareTo(), which EF does translate, so the comparison ran in the database as intended instead of failing or falling back to slow client-side evaluation.
A DateTimeOffset ↔ timestamp mapping bug: Npgsql is strict about how DateTimeOffset/DateTime map to timestamp versus timestamptz, and the column type didn't match the intent, so a round-trip risked losing or misreading timezone information. I traced it to the mapping, set the correct timestamptz column type in the model, adjusted the EF Core migration so datetimes carry proper timezone semantics, and applied it cleanly against existing data.
Outcome: queries execute at the database level where they should, and timestamps store and read back with correct timezone semantics, two commonly-botched areas, fixed at the source rather than papered over.5. Credentials that load correctly in every environmentWe authenticated to Google Sheets with a service account, but the credentials lived differently in each environment: a file path locally, raw JSON on Azure. That's exactly the kind of split that produces environment-specific "works on my machine" deployment bugs. I refactored credential loading into a shared utility that detects whether the env var holds raw JSON or a file path, validates it, fails with a clear error when it's missing or malformed, and is centralised so every service that needs it reuses the same logic.Outcome: deployments stopped carrying environment-specific credential bugs, and standing the system up in a new environment got noticeably less fiddly, the robust path instead of the brittle one.6. Shipping and operating it in productionBuilding it was half the job; keeping it running and cheap was the other. Three runtimes, three deployment models: a Node/Next.js dashboard and a .NET API on App Service, and a Python container on Container Apps behind its own registry, wired together with JWT auth, environment-injected secrets, and a Neon-hosted Postgres. Beyond the hourly OCR cycle, a second pair of Logic Apps starts the front and back-end servers in the morning and stops them in the evening, so nothing bills overnight. When a run misbehaves, I diagnose it from the Container App console logs and redeploy the affected service on its own, without touching the rest.Outcome: a production system I can operate and cost-tune day to day, running a manual daily chore entirely on its own.
What I shipped
Three independently deployed services: Next.js dashboard, .NET backend, Python OCR microservice
Authenticated dashboard to browse sourced vehicles, filter by processing status, and inspect a record
Automated hourly scrape of new AutoTrader listings, deduplicated and safe to re-run
Number-plate reading from vehicle photos: YOLOv8 detection + PaddleOCR + UK-plate correction
Record enrichment from external history & valuation services
Safe, incremental import API: input validation, dry-run mode, marker-based reruns
PostgreSQL storage via EF Core with correct timezone-aware type mapping, plus automatic Google Sheets sync as the business output
Environment-aware credential loading shared across services
Scale-to-zero OCR container, started and stopped hourly by an Azure Logic App via Managed Identity
Morning/evening server start-stop automation for cost control
Deployed to Azure across App Service, Container Apps, and Container Registry
Running live in production, replacing a manual daily research-and-data-entry process
Reflection
Automating this workflow meant owning every layer, from a YOLO crop and an OCR correction rule, to an EF Core migration, to a Google Sheets append, to a Logic App that starts a container with a Managed Identity token. As with most real systems, the biggest lessons were in the unglamorous parts: what an ORM can and can't push to the database, getting timezone types right across a boundary, loading credentials the same way in every environment, and making reruns safe by default. I lean on modern tooling, including AI pair-programming, to move quickly, but every architecture decision, integration, and bug fix went through my own understanding and testing. That's how a chore someone used to do by hand every morning became a system that just runs.
Status: Live in production for Position One Motor Company.
Available for full-stack / cloud automation freelance work.Nick Ayoola · Email · LinkedIn










