PredictHub is a sports prediction marketplace where football fans publish match picks (win/draw/BTTS/over-under), build a public reputation, and earn from being right. Predictions are tied to live matches sourced from The Odds API and auto-resolved by a cron job every 30 minutes. Users follow top predictors, compete on a global leaderboard, and interact through a social feed — the feel of Twitter-for-tipsters. A wallet system handles subscription payments and withdrawals. An AI scoring module (Claude API) evaluates prediction quality. I was the sole engineer — responsible for architecture, all three apps, database schema, auth flow, and deployment.
The Hard Architectural Problem
Supabase RLS assumes auth.uid() comes from a Supabase session token — but the app issues its own JWTs via NestJS Passport, making auth.uid() always null and silently blocking every write and owner-scoped read.
The fix was a deliberate architectural rule enforced at the service layer: all DB calls go through the service-role admin client (bypassing RLS entirely), while RLS remains active only at the edge for any direct client access. This eliminates the auth.uid() null problem without disabling RLS globally — a clean boundary that keeps the security model intact.
A second class of bug: PostgREST ambiguous foreign key paths across junction tables (predictions ↔ users) required explicit FK hints on every embed query. These fail silently at query time with opaque 406 errors — found and fixed by reading PostgREST source behaviour, not the docs.
Architecture Pattern
NestJS Passport JWT · Supabase RLS bypass via service-role admin client · PostgREST explicit FK hints · 30-min cron result polling · OTP-gated auth via Resend API
What I Built
01
OTP-Gated Authentication
Register and login both require email OTP verification before a JWT is issued — no password stored.
Welcome email sent on first verify via Resend transactional API (6-digit OTP, 10-min expiry).
NestJS Passport strategy validates the custom JWT on every protected route.
02
Prediction Engine & Cron Resolution
Users create picks tied to live matches fetched from The Odds API (match existence validated on creation).
A 30-minute cron job polls match results and auto-resolves open predictions — win, loss, or void.
Prediction history, win rates, and streaks update in real time after each resolution cycle.
03
Social Feed, Likes & Comments
Paginated activity feed showing predictions from followed users and trending picks.
Likes, comments, and nested replies with optimistic UI updates on the frontend.
Follow/unfollow system with follower counts and mutual-follow detection.
04
Wallet & Transaction System
Wallet auto-created on user signup with zero balance.
Deposit, withdraw, and subscription payment flows with full transaction history.
Admin approval gate on withdrawal requests before funds are released.
05
Leaderboard & Referral Program
Global leaderboard ranked by win rate and prediction volume, filterable by sport.
Invite-only referral system — links tracked to referrer with conversion metrics surfaced in admin.
06
Admin Dashboard
Separate authenticated Vite app (port 5174) with its own auth boundary.
User moderation: verify, ban, flag accounts; view full prediction and transaction history per user.
Withdrawal approval workflow, transaction oversight, and overview charts (Recharts).
Referral conversion metrics and invite analytics.
07
AI Prediction Scoring Module
Scaffolded Claude API integration to evaluate prediction quality against historical odds and outcomes.
Pluggable architecture — the scoring module runs as an independent service, callable by the cron pipeline.
Designed to surface a quality score per predictor, surfaced on the leaderboard.
Database Architecture
13 migration files across a normalised PostgreSQL schema. Key design decisions:
Junction tables for predictions ↔ matches and users ↔ followers — explicit FK hints on all PostgREST embeds.
Row-level security policies defined per table, enforced only at the edge (direct client access).
Service-role admin client used exclusively at the NestJS service layer — no RLS friction on writes.
Wallet and transaction tables fully separated from user auth — allows independent balance reconciliation.
Business Impact
0→prod
in one build sprint
<2s
OTP email delivery
13
DB migration files
All core flows verified end-to-end — zero TypeScript errors, zero RLS leakage on clean Render deploy.
OTP email delivered via Resend in under 2s average — no auth friction at onboarding.
Cron match resolution runs 8 result checks per day, within The Odds API free tier with headroom.
Full admin moderation and payment workflow in place — no manual DB access needed for operations.