Skip to main content
CodePlanet Docs

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 /docs

Route 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

On this page