Skip to main content
Back to Blog

How we built ReviseWizard on Supabase

25 May 2026
12 views

ReviseWizard is a free platform for UK students searching for degree apprenticeships, apprenticeships, and internships. The idea came from my own experience as a student trying to track applications across ten different employer portals, a spreadsheet, and an inbox that was constantly getting out of hand. The platform aggregates live roles in one place and gives students a tracker to move opportunities from saved through to offer.

I built it as a solo founder, which means every infrastructure decision matters twice as much. You are not choosing tools academically. You are choosing what you will actually be able to maintain, debug, and extend at 11pm when something breaks.

Supabase is our entire backend.

Why Supabase

I wanted Postgres. Not a document store, not a key-value layer on top of something else. Relational data with proper foreign keys, joins, and migrations. Opportunity listings have sectors, locations, salary ranges, deadlines, and employer metadata. A tracker ties a user to an opportunity with a stage, notes, and a timeline. That kind of structure needs a real schema.

The alternative was spinning up my own Postgres instance and wiring up auth, storage, and an API layer from scratch. For a solo project serving students for free, that is a maintenance burden that does not make sense. Supabase gave me the database I wanted with auth, row-level security, edge functions, and a typed client already attached to it.

The database

The core schema has three main areas: opportunities, users, and the tracker.

Opportunities are scraped from employer portals and stored in a central table. Each row holds the role title, employer name, location, salary, deadline, type (degree apprenticeship, apprenticeship, or internship), sector, and a scraped_at timestamp. We mark rows as is_active false rather than deleting them, which keeps historical data intact and makes the sitemap generation more reliable.

The tracker ties a user ID to an opportunity ID with a stage column that moves through states like saved, applied, interview, and offer. Users can attach notes to each entry. Everything is joined through foreign keys so queries stay clean and there is no risk of orphaned rows.

Migrations live in supabase/migrations and are never edited after the fact. Every schema change is a new file. That discipline has saved me more than once when I needed to understand exactly when a column was added or a constraint was changed.

Authentication

We use Supabase Auth with email and password sign-in. The useSession hook handles auth state on the client and there is no duplicate auth logic anywhere in the codebase. That was a deliberate rule from early on.

The service role key only ever exists on the server side. It is never imported into anything that could be bundled into the client. API routes in Next.js use it directly. The client gets the anon key. It sounds obvious but it is the kind of thing that is easy to get wrong under deadline pressure and hard to audit after the fact.

Row-level security

Every table that holds user data has RLS enabled. No exceptions. This means a user can only ever read or write their own tracker entries, their own notes, and their own profile data. The database enforces this at the query level rather than relying on the application layer to remember to filter correctly.

For the opportunities table, reads are public since anyone can browse listings. Writes go through server-side API routes only and are protected by the service role key.

Edge Functions for scraping

The most interesting part of the Supabase setup is how we keep listings up to date. We run two Edge Functions in production: sync-opportunities and sync-internships. Both run on a 30-minute cron schedule.

Each function scrapes employer portals and job boards, normalises the data into our schema, and upserts rows using a deduplication key based on the role title and employer. If a listing disappears from the source, we mark it inactive rather than deleting it. If it comes back, we reactivate it.

Running the scrapers as Edge Functions rather than a separate service kept the infrastructure simple. There is no separate scraper server to provision, monitor, or pay for. The functions run close to the database, the logs are in one place, and deployment is part of the same pipeline as everything else.

What worked well

Supabase's TypeScript client is good. Not perfect, but good enough that you rarely have to fight it. The generated types from the schema mean TypeScript catches a large class of bugs before they reach production. When you rename a column in a migration and forget to update a query, the type checker tells you immediately.

The local development stack is also straightforward. supabase start spins up a local Postgres instance with the full suite of services, and supabase db push applies migrations to the remote project. For a solo project this workflow is genuinely fast.

What to watch out for

PostgREST, which is the API layer Supabase puts in front of Postgres, caps rows per request at 1,000 by default regardless of what you pass to .limit(). We hit this in the sitemap generator when we tried to fetch all active listings in one query. The fix was paginating in 1,000-row batches until the response comes back with fewer rows than the page size. It is documented but easy to miss.

RLS policies also need careful thought when you have multiple roles interacting with the same table. We spent some time debugging why a server-side route was returning empty results before realising the policy was being applied to the service role in a way we had not anticipated. Reading the Supabase RLS documentation properly before writing policies saves time later.

Where we are now

ReviseWizard is live and serving students across the UK. The platform stays free, the scrapers run every half hour, and Supabase handles everything underneath without requiring me to think about it most of the time. For a solo project with a student audience that cannot be charged, that operational simplicity is not a nice-to-have. It is what makes the whole thing viable.

If you are building something similar and want to talk through the architecture, you can reach me at yusuf@revisewizard.com.

About

Who built Apprentice Wizard?

Read Yusuf's story, why Apprentice Wizard exists, and what's being built next.

About the founder

Explore

Ready to find your role?

Search, save, and track apprenticeships and internships — all in one place.

Browse Opportunities