A modern SQL preparation platform: practice problems, real-world case studies, interview questions, and data-engineering system-design walkthroughs — each with a guided approach and complete solution. Built with the latest tech stack for a fast, delightful experience.
- 500 Practice Problems — Easy / Medium / Hard, from basic
SELECTs to advanced window functions, recursive CTEs, JSON, A/B tests, fraud detection, time-series, and more. Each has an in-browser SQL playground with real test cases. - 100 Case Studies — Real business scenarios (e-commerce, SaaS, fintech, healthcare, marketplaces) with a "Thinking Process" and complete solution.
- 170 Interview Questions — Conceptual + code-answer prep on joins, indexes, transactions, isolation, replication, partitioning, MVCC, and everything else that comes up.
- 140 Data-Engineering System Design Problems — Full architectures for warehouses, streaming pipelines, ML platforms, and FAANG-tagged interview asks (DoorDash, Uber, Airbnb, TikTok, Snap, Spotify, etc.). Each includes clarifying questions, key parameters, and a complete reference architecture.
- In-browser SQL Playground — Run queries against seeded SQLite databases via
sql.js(WebAssembly). No backend needed. - Progress Tracking — Solved / attempted counts, streaks, activity log, and category-level progress bars in the dashboard.
- Bookmarks — Save questions for revision.
- Row Level Security — All user tables enforce per-user isolation via Supabase RLS.
- Dark / Light Mode — Auto-follows system, manually toggleable.
- Topic & Difficulty Filters — Quickly narrow down what you want to practice.
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) + React 19 |
| Language | TypeScript 5.7 (strict) |
| Styling | Tailwind CSS v3 + shadcn/ui |
| Database | Supabase Postgres |
| ORM | Drizzle ORM |
| Auth | Supabase Auth (email + magic link) |
| SQL Playground | sql.js (SQLite in WebAssembly) |
| Code Editor | Monaco Editor |
| Deployment | Vercel |
npm installpostinstall copies the sql.js WASM files into public/.
- Go to supabase.com and create a free project.
- From Settings → API copy:
- Project URL
anonpublic key
- From Settings → Database → Connection string copy the Transaction Pooler URI (port 6543) — this is what you should use in production on Vercel to avoid exhausting the connection pool.
cp .env.example .env.localFill in:
NEXT_PUBLIC_SUPABASE_URL=https://YOUR-PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
DATABASE_URL=postgresql://postgres.YOUR-PROJECT:PASSWORD@aws-0-REGION.pooler.supabase.com:6543/postgres
DATABASE_URLis server-only. It bypasses RLS by design — API routes filter byuser.idfromauth.getUser()before returning rows. Never importlib/dbfrom a client component.
npm run db:pushCreates all tables (profiles, questions, bookmarks, progress, notes, streaks, activity_log) plus enums.
npm run db:rlsEnables RLS on every table and installs per-user policies plus the on_auth_user_created trigger that auto-creates a profiles row on signup. Verify with:
npm run db:verify-rlsnpm run db:seedUpserts all ~910 MDX files (500 problems + 100 case studies + 170 interview + 140 system-design) into the questions table. The API routes look up questions by slug, so the app will 404 on Submit/Bookmark until this is run.
Re-run any time you add new content files.
npm run devOpen http://localhost:3000.
sql-prep/
├── app/ # Next.js App Router
│ ├── problems/ # Practice problems list + detail
│ ├── case-studies/ # Case studies list + detail
│ ├── interview/ # Interview questions list + detail
│ ├── system-design/ # System-design list + detail
│ ├── dashboard/ # User progress dashboard
│ ├── bookmarks/ # Saved questions
│ ├── sign-in/, sign-up/ # Auth pages
│ ├── auth/callback/ # OAuth/magic-link callback
│ ├── api/
│ │ ├── bookmarks/ # REST API for bookmarks
│ │ ├── progress/ # REST API for solve/review tracking
│ │ └── health/ # Health probe for uptime checks
│ ├── error.tsx # Route-level error boundary
│ ├── global-error.tsx # Global error boundary
│ ├── not-found.tsx
│ ├── layout.tsx
│ └── page.tsx # Landing page
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── content-detail.tsx # Detail page shell w/ tabs + playground
│ ├── sql-editor.tsx # Monaco + sql.js playground
│ ├── mark-reviewed-button.tsx # Progress marker for non-playground items
│ └── ...
├── content/
│ ├── problems/*.mdx # 500 problems
│ ├── case-studies/*.mdx # 100 case studies
│ ├── interview/*.mdx # 170 interview questions
│ └── system-design/*.mdx # 140 system-design problems
├── lib/
│ ├── db/ # Drizzle schema + client
│ ├── supabase/ # SSR + browser + middleware clients
│ ├── content.ts # MDX loader & parser
│ ├── sql-runner.ts # sql.js wrapper
│ ├── user-data.ts # Server-side progress/stats helpers
│ └── utils.ts # Includes safeRedirectPath()
├── drizzle/
│ └── rls.sql # RLS policies + signup trigger
├── scripts/
│ ├── copy-wasm.mjs # Runs on postinstall
│ ├── seed-questions.mjs # Content -> DB
│ ├── apply-rls.mjs # RLS bootstrap
│ ├── verify-rls.mjs # RLS status check
│ └── add-system-design-enum.mjs
├── middleware.ts # Auth-gates all non-public routes
├── drizzle.config.ts
├── tailwind.config.ts
├── next.config.mjs # Security headers included
└── package.json
Each piece of content is a single MDX file. The frontmatter defines metadata; the body has three sections. After adding files, run npm run db:seed to sync to the database.
---
title: "Your Problem Title"
difficulty: easy | medium | hard
topic: "Joins" # any string, used for filtering
tags: ["INNER JOIN", "GROUP BY"] # optional array
description: "Short one-liner"
schema: | # optional; enables the playground
CREATE TABLE ...
INSERT INTO ...
expectedOutput: # optional; enables Submit validation
columns: [id, name]
rows:
- [1, "Alice"]
- [2, "Bob"]
---
## Problem
The problem statement...
## Approach
1. First think about...
2. Then use...
## Solution
```sql
SELECT ...
### Case Studies (`content/case-studies/*.mdx`)
Same shape, tabs labeled "Thinking Process" and "Solution". Typically omit `difficulty` and playground schema.
### Interview Questions (`content/interview/*.mdx`)
Same shape, tabs labeled "Basic Approach" and "Full Answer". Playground is optional.
### System Design (`content/system-design/*.mdx`)
Same shape but the solution section is headed `## Architecture` (both are recognized). Playground is disabled; users mark items as Reviewed instead of Solved.
## Available Scripts
| Script | Purpose |
| ------------------------------- | ------------------------------------------------------------------ |
| `npm run dev` | Start dev server on port 3000 |
| `npm run build` | Production build |
| `npm run start` | Serve the production build |
| `npm run lint` | Run ESLint |
| `npm run db:generate` | Generate a Drizzle migration from schema.ts |
| `npm run db:push` | Push schema directly to Supabase |
| `npm run db:studio` | Open Drizzle Studio (visual DB browser) |
| `npm run db:rls` | Apply RLS policies + signup trigger |
| `npm run db:verify-rls` | Verify RLS is enabled on every table |
| `npm run db:seed` | Sync all MDX content into the `questions` table |
| `npm run db:add-system-design` | One-off migration to add `system_design` to the content_type enum |
## Deployment (Vercel)
1. Push to a GitHub repo.
2. Import the repo in Vercel.
3. In **Project Settings → Environment Variables**, add:
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `DATABASE_URL` — **use the Supabase Transaction Pooler URL (port 6543)** for serverless environments. The Session Pooler (5432) can exhaust connections quickly under a burst of cold starts.
4. Deploy.
5. From your local machine (with the same `.env.local`), run against the production database:
```bash
npm run db:push # if not already pushed
npm run db:rls # required
npm run db:seed # required — populates all 910 questions
Detail pages are declared export const dynamic = "force-dynamic" so user-specific "Solved / Reviewed" state is always fresh.
- All user tables have RLS enabled with per-user policies (see
drizzle/rls.sql). - Every
/api/*route callssupabase.auth.getUser()and scopes writes/reads byuser.id. - The middleware redirects unauthenticated users to
/sign-infor every non-public route. - Redirect params (
?redirect=...) are validated bysafeRedirectPath()inlib/utils.tsto prevent open-redirect attacks. - Markdown rendering escapes all user-supplied text; code-fence language attributes are strictly
[a-zA-Z0-9_-]. - API body payloads (
slug,query) are length- and format-validated. - Security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) are set in
next.config.mjs.
- 500 problems / 100 case studies / 170 interview questions / 140 system-design walkthroughs
- RLS on every table + signup trigger
- Progress tracking (solve / review) + streaks + activity log
- Local bookmarks with best-effort DB sync
- Server-authoritative bookmarks (cross-device sync on read)
- Rate limiting on
/api/progressand/api/bookmarks - Full-text search across all content
- PDF export of bookmarks for offline revision
- Weekly challenges & leaderboards
MIT — see LICENSE.