How we built a "Duolingo for SQL" as a solo side project and got 48 signups on day one
The Problem: SQL Practice That Doesn't Suck
Let's be honest - existing SQL practice platforms kind of miss the mark. You've got W3Schools, which is great for absolute beginners but gets boring fast. Then there's LeetCode and HackerRank, which throw you into the deep end with complex interview questions that make you question your entire career.
But what about those of us who just want to stay sharp? Who want to practice SQL daily without the pressure of interview prep or the boredom of SELECT * FROM users
for the hundredth time?
I wanted something like Duolingo for SQL - bite-sized daily practice that keeps skills fresh. Or Chess.com for queries - where you can jump in, solve a puzzle, track your progress, and actually enjoy the process. When I couldn't find it, I decided to build it.
Enter InnerJoin - a SQL practice platform I built solo over 3 months while running South Shore Analytics. Here's how it went down, what worked, what didn't, and what 48 first-day users taught us about product-market fit.
Tech Stack Choices: Boring is Beautiful
Backend: FastAPI (Python)
I went with FastAPI over Django/Flask for a few reasons:
- Type hints out of the box - catches bugs before they hit production
- Automatic API documentation - Swagger UI is a lifesaver for debugging
- Async support - crucial for handling multiple SQL query executions
- Performance - benchmarks showed 2x speed improvement over Flask for our use case
@app.post("/api/challenges/{challenge_id}/submit")
async def submit_solution(
challenge_id: int,
submission: QuerySubmission,
current_user: User = Depends(get_current_user)
):
# FastAPI handles validation, auth, and serialization
result = await execute_sandboxed_query(submission.query)
score = calculate_score(result, submission.time_taken)
return {"result": result, "score": score}
Frontend: React + TypeScript
Nothing fancy here - React with TypeScript for component reusability and type safety. The SQL editor uses CodeMirror with custom PostgreSQL syntax highlighting. Material-UI for components because I wanted to ship, not design a button for 3 days.
Database: PostgreSQL 15
This was non-negotiable. Users write real SQL against real databases. No regex matching or string comparison nonsense. Each challenge gets its own schema with sample data, isolated from production.
Deployment: Railway
Docker deployment in literally 3 clicks. Connected to GitHub, push to main, deployed. No Kubernetes complexity, no AWS billing surprises. Just works.
The Rest
- PostHog for analytics (open-source, privacy-focused)
- Stripe for payments
- SendGrid for transactional emails
Biggest Technical Challenges
1. SQL Execution Sandboxing (The Security Nightmare)
Letting users execute arbitrary SQL is basically asking to get hacked. Here's how I solved it:
def create_sandboxed_connection(challenge_id: int):
# Each challenge gets a read-only user
sandbox_user = f"challenge_{challenge_id}_readonly"
# Connect with restricted permissions
conn = psycopg2.connect(
dbname="challenges_db",
user=sandbox_user,
password=generate_temp_password(),
options="-c statement_timeout=5000" # 5 second timeout
)
# Set transaction to read-only
conn.set_session(readonly=True)
return conn
Each challenge runs in its own PostgreSQL schema with:
- Read-only permissions
- 5-second query timeout
- Resource limits (max 100MB memory)
- No access to system tables
- Isolated data that resets after each attempt
2. ELO Rating System
I wanted skill-based matching like Chess.com. Users start at 1200 ELO, challenges have fixed ratings. Win against a harder challenge? Big ELO gain. Lose to an easy one? Rating drops.
def calculate_elo_change(user_rating: int, challenge_rating: int, won: bool) -> int:
K = 32 # K-factor for rating volatility
expected = 1 / (1 + 10 ** ((challenge_rating - user_rating) / 400))
actual = 1 if won else 0
return round(K * (actual - expected))
3. Challenge Difficulty Matching
Nobody wants to face impossible challenges on day one. The algorithm considers:
- User's current ELO rating
- Recent performance (last 5 challenges)
- Topics they've practiced
- A bit of randomness to keep it interesting
4. Time-Based Scoring
Borrowed from competitive programming - you get full points for solving within 5 minutes, with a sliding scale down to 50% at 10 minutes. Fast, correct solutions = higher scores.
def calculate_time_bonus(seconds_taken: int) -> float:
if seconds_taken <= 300: # Under 5 minutes
return 1.0
elif seconds_taken <= 600: # 5-10 minutes
return 1.0 - (seconds_taken - 300) / 600
else:
return 0.5 # Minimum 50% score
Launch Results - Day 1: Holy Crap, People Actually Want This
We soft-launched on Monday with a single post on LinkedIn. The results blew my mind:
- 48 signups in 24 hours
- 48% activation rate (23 users completed at least one challenge)
- 0 critical bugs (but a few UI suggestions!)
- 8 feedback messages / emails (all constructive!)
The top feedback? "Why can I only practice one challenge per day? I want MORE!"
That's what makes me believe we have something. People aren't just signing up - they're actively engaging and asking for more. The 48% activation rate particularly stood out. Industry average for SaaS products is around 20-30%. Nearly half of signups immediately engaging? That feels like the early stages of product market fit.
Key Learnings: What 3 Months Taught Me
Ship Early, Perfect Never
I launched with 25 challenges. Not 100, not 1000. Twenty-five. And you know what? Nobody complained about the quantity - they just wanted to do more per day. Perfectionism would have killed this project.
Simple Gamification Works
Streaks, ELO ratings, and daily challenges. That's it. No complex achievement system, no badges (yet). Users message me about maintaining their streaks. Sometimes simple is exactly right.
Users Are Your Best Product Managers
That feedback about unlimited practice? It will be the core of the paid tier. Users are literally designing our monetization strategy for us. All we have to do is listen.
Technical Debt is Fine (Initially)
My codebase isn't pretty. There's a 200-line function that makes me cringe. The frontend has prop drilling issues. But users don't care about your code quality (within reason) - they care about solving their problems. Ship first, refactor later.
Beta Users Are Gold
Offering 100 free lifetime accounts in exchange for feedback was the best decision we could have made. These users found bugs, suggested features, and became evangelists. The value of their input far exceeds any future subscription revenue.
What's Next: The Roadmap
Based on user feedback, here's what's coming:
Unlimited Practice Mode (Paid Tier)
- Solve as many challenges as you want
- Access to challenge library with filters
- Advanced analytics on your progress
30-Day SQL Beginner Course
- Structured learning path for newcomers
- Interactive lessons with immediate practice
- Certificate of completion (people love certificates)
Quality of Life Features
- Daily email reminders (optional)
- Dark mode (the most requested feature, naturally)
- Solution explanations with multiple approaches
- Time-based leaderboards for competitive folks
Scale
- Expanding from 25 to 100+ challenges
- Adding window functions, CTEs, and advanced topics
- Industry-specific challenge sets (finance, e-commerce, healthcare)
The Real Talk
This project took a couple hundred hours over 3 months. Nights, weekends, and way too much coffee. There were moments where I questioned whether anyone would actually use this. Imposter syndrome hit hard - who are we to teach SQL when there are platforms backed by millions in funding?
But sometimes the best products come from scratching your own itch. We built InnerJoin because we wanted it to exist. The fact that 48 other people wanted it too on day one? That's validation enough.
Want to Try It? (The Shameless Plug)
The first 100 users get lifetime free access. No credit card required, no catch. I need beta testers, and you need SQL practice. Win-win.
👉 innerjoin.southshoreanalytics.com
Currently at 52/100 beta users, so grab your spot while you can.
Let's Connect
Building in public means sharing the journey - the good, the bad, and the "why is this bug only happening in production?" moments.
Got questions about the tech stack? Want to know more about specific implementation details? Thinking about building your own SaaS? Drop a comment below or reach out. I'm an open book.
And if you're one of those 48 day-one users reading this - thank you. You validated months of work and gave me the motivation to keep building.
Now if you'll excuse me, I have a dark mode feature to implement. 🌙
Tags: #sql #webdev #showdev #postgres #buildinpublic #saas #react #python #fastapi
Follow my journey building InnerJoin and other data tools at South Shore Analytics. Next update (hopefully): what happens when you hit the front page of HackerNews (spoiler: your server melts).
Top comments (0)