Backend Services
APIs, database, and infrastructure
CodePlanet's backend is built on Next.js API routes and Supabase, providing a scalable, secure, and developer-friendly architecture.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ Next.js API Routes │
│ /api/v1/* │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────┐ │
│ │ Middleware │─▶│ Validation │─▶│ Route Handler │ │
│ │ (Auth, Rate) │ │ (Input) │ │ (Business Logic) │ │
│ └────────────────┘ └────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Supabase │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────┐ │
│ │ PostgreSQL │ │ Auth │ │ Storage │ │
│ │ + RLS │ │ (JWT/SSO) │ │ (Files/Images) │ │
│ └────────────────┘ └────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘API Structure
All API routes are under /api/v1/ for versioning:
src/app/api/v1/
├── problems/
│ ├── route.ts # GET /problems (list)
│ └── [slug]/
│ ├── route.ts # GET /problems/:slug
│ └── submit/
│ └── route.ts # POST /problems/:slug/submit
├── learning/
│ └── weak-topics/
│ └── route.ts # GET, POST /learning/weak-topics
├── payments/
│ ├── create/route.ts # POST /payments/create
│ ├── verify/route.ts # POST /payments/verify
│ ├── webhook/route.ts # POST /payments/webhook
│ └── upi/route.ts # POST /payments/upi
├── user/
│ └── route.ts # GET, PATCH /user/me
└── docs/
└── route.ts # GET /docsRoute Handler Pattern
Every route follows a consistent pattern:
// src/app/api/v1/example/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
export async function GET(request: NextRequest) {
try {
// 1. Authentication
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
// 2. Input Validation
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get("page") || "1");
if (isNaN(page) || page < 1) {
return NextResponse.json(
{ success: false, error: "Invalid page parameter" },
{ status: 400 }
);
}
// 3. Business Logic
const { data, error } = await supabase
.from("table")
.select("*")
.eq("user_id", user.id)
.range((page - 1) * 20, page * 20 - 1);
if (error) throw error;
// 4. Response
return NextResponse.json({
success: true,
data,
});
} catch (error) {
console.error("API Error:", error);
return NextResponse.json(
{ success: false, error: "Internal server error" },
{ status: 500 }
);
}
}Supabase Integration
Server Client
// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}Admin Client
For operations requiring elevated privileges:
// src/lib/supabase/admin.ts
import { createClient } from "@supabase/supabase-js";
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Never expose this!
{ auth: { persistSession: false } }
);Database Schema
Core Tables
-- Users (extended from auth.users)
CREATE TABLE public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id),
name TEXT,
avatar_url TEXT,
bio TEXT,
plan TEXT DEFAULT 'free',
xp INTEGER DEFAULT 0,
streak INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Submissions
CREATE TABLE public.submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
problem_slug TEXT NOT NULL,
language TEXT NOT NULL,
code TEXT NOT NULL,
status TEXT NOT NULL,
runtime_ms INTEGER,
memory_mb DECIMAL(10,2),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Topic Performance (for weak topic detection)
CREATE TABLE public.topic_performance (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
topic TEXT NOT NULL,
total_attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
weakness_score DECIMAL(5,2) DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
UNIQUE(user_id, topic)
);Row-Level Security
Every table has RLS policies:
-- Enable RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Users can only access their own profile
CREATE POLICY "Users can view own profile"
ON public.profiles
FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON public.profiles
FOR UPDATE
USING (auth.uid() = id);Input Validation
All inputs are validated before processing:
// UUID validation
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!UUID_REGEX.test(userId)) {
return NextResponse.json(
{ success: false, error: "Invalid user ID format" },
{ status: 400 }
);
}
// Enum validation
const VALID_DIFFICULTIES = ["easy", "medium", "hard"] as const;
type Difficulty = typeof VALID_DIFFICULTIES[number];
if (!VALID_DIFFICULTIES.includes(difficulty as Difficulty)) {
return NextResponse.json(
{ success: false, error: "Invalid difficulty" },
{ status: 400 }
);
}Rate Limiting
Rate limiting is implemented at the API level:
// Using in-memory store (for development)
// In production, use Redis or similar
import { LRUCache } from "lru-cache";
const rateLimit = new LRUCache<string, number[]>({
max: 10000,
ttl: 60 * 1000, // 1 minute
});
export async function checkRateLimit(
identifier: string,
limit: number = 100
): Promise<boolean> {
const now = Date.now();
const windowStart = now - 60 * 1000;
const timestamps = rateLimit.get(identifier) || [];
const recentTimestamps = timestamps.filter(t => t > windowStart);
if (recentTimestamps.length >= limit) {
return false; // Rate limited
}
recentTimestamps.push(now);
rateLimit.set(identifier, recentTimestamps);
return true;
}Error Handling
Consistent error response format:
// Error types
type APIError = {
code: string;
message: string;
details?: Record<string, unknown>;
};
// Helper function
function errorResponse(
error: APIError,
status: number
): NextResponse {
return NextResponse.json(
{ success: false, error },
{ status }
);
}
// Usage
return errorResponse(
{
code: "VALIDATION_ERROR",
message: "Invalid input",
details: { field: "email", reason: "Invalid format" }
},
400
);External Services
Razorpay
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
// Create order
const order = await razorpay.orders.create({
amount: 79900,
currency: "INR",
receipt: `order_${Date.now()}`,
});Email (Transactional)
// Using Resend or similar
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: "CodePlanet <noreply@acodeplanet.tech>",
to: user.email,
subject: "Welcome to CodePlanet",
html: welcomeEmailTemplate(user.name),
});Environment Variables
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Razorpay
RAZORPAY_KEY_ID=rzp_test_xxx
RAZORPAY_KEY_SECRET=xxx
RAZORPAY_WEBHOOK_SECRET=xxx
# General
NODE_ENV=production
Next Steps
- Data Flow — How data moves through the system
- Security Overview — Security practices
- API Endpoints — Full API reference