Codapult
FeaturesPricingAPIHelpChangelog
Codapult

Ship Your SaaS Faster

Product

  • Features
  • Pricing
  • Plugins
  • API Reference
  • Help Center
  • Feature Requests
  • Changelog

Company

  • Contact
  • GitHub

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Codapult. All rights reserved.

All articles

Getting Started

  • Introduction
  • Quick Start
  • Project Structure

Configuration

  • Environment Variables
  • App Configuration

Authentication

  • Authentication

Database

  • Database

Teams

  • Teams & Organizations

Payments

  • Payments & Billing

Api

  • API Layer

Ai

  • AI Features

Email

  • Email

Infrastructure

  • Infrastructure

Ui

  • UI & Theming

I18n

  • Internationalization

Content Management

  • Content Management

Admin

  • Admin Panel

Security

  • Security

Monitoring

  • Analytics & Monitoring

Modules

  • Module Architecture

Plugins

  • Plugin System

Deployment

  • Deployment
  • Troubleshooting

Upgrading

  • Upgrading Codapult

Developer Tools

  • MCP Server
  • Testing
Api

API Layer

API routes, tRPC, GraphQL, server actions, versioning, and rate limiting.

Codapult provides four complementary ways to expose server logic: REST API routes, tRPC, GraphQL, and server actions. All share the same auth, validation, and rate-limiting primitives.

API Routes

All routes live in src/app/api/ and must follow a strict five-step pattern:

import { NextResponse } from 'next/server';
import { getAppSession } from '@/lib/auth';
import { checkRateLimit } from '@/lib/rate-limit';
import { someSchema } from '@/lib/validation';

export async function POST(req: Request) {
  // 1. Auth check
  const session = await getAppSession();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 2. Rate limiting
  const { allowed, remaining, resetAt } = checkRateLimit(`scope:${session.user.id}`, {
    limit: 30,
    windowSeconds: 60,
  });
  if (!allowed) {
    return NextResponse.json(
      { error: 'Too many requests. Please wait a moment.' },
      {
        status: 429,
        headers: {
          'Retry-After': String(Math.ceil((resetAt - Date.now()) / 1000)),
        },
      },
    );
  }

  // 3. Input validation (Zod)
  const body = await req.json();
  const data = someSchema.parse(body);

  // 4. Business logic
  const result = await doSomething(data);

  // 5. Response
  return NextResponse.json({ result }, { headers: { 'X-RateLimit-Remaining': String(remaining) } });
}

Rules:

  • Always return { error: string } for errors — never expose stack traces or internal details.
  • Attach rate-limit headers: X-RateLimit-Remaining and Retry-After.
  • Use Zod schemas from @/lib/validation.ts for input validation.

tRPC

Codapult includes a full tRPC v11 setup with TanStack React Query and superjson serialization.

Structure

PathDescription
src/lib/trpc/init.tsRouter factory, procedure levels (publicProcedure, protectedProcedure, adminProcedure)
src/lib/trpc/routers/Domain routers — user.ts, billing.ts, notifications.ts
src/app/api/trpc/[trpc]/route.tsHTTP endpoint at /api/trpc

Procedure Levels

LevelAuth RequiredUse Case
publicProcedureNoPublic data (plans, features)
protectedProcedureYesUser-scoped data (profile, settings)
adminProcedureYes + admin roleAdmin operations (user management)

Creating a Router

import { z } from 'zod';
import { router, protectedProcedure } from '../init';
import { db } from '@/lib/db';
import { user } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

export const userRouter = router({
  me: protectedProcedure.query(async ({ ctx }) => {
    const rows = await db
      .select({ id: user.id, name: user.name, email: user.email })
      .from(user)
      .where(eq(user.id, ctx.session.user.id))
      .limit(1);
    return rows[0] ?? null;
  }),

  updateProfile: protectedProcedure
    .input(z.object({ name: z.string().min(1).max(100) }))
    .mutation(async ({ ctx, input }) => {
      await db
        .update(user)
        .set({ name: input.name, updatedAt: new Date() })
        .where(eq(user.id, ctx.session.user.id));
      return { success: true };
    }),
});

Server-Side Prefetch

Use prefetch() in server components and hydrate on the client:

import { prefetch } from '@/lib/trpc/server';
import { HydrateClient } from '@/lib/trpc/client';

export default async function ProfilePage() {
  await prefetch((t) => t.user.me.prefetch());

  return (
    <HydrateClient>
      <ProfileClient />
    </HydrateClient>
  );
}

Client-Side Usage

'use client';

import { useTRPC } from '@/lib/trpc/client';
import { useQuery } from '@tanstack/react-query';

export function ProfileClient() {
  const trpc = useTRPC();
  const { data: user } = useQuery(trpc.user.me.queryOptions());

  return <h1>Hello, {user?.name}</h1>;
}

GraphQL

An optional graphql-yoga server is available at /api/graphql.

PathDescription
src/lib/graphql/schema.tsSDL type definitions
src/lib/graphql/resolvers.tsResolver implementations
src/app/api/graphql/route.tsHTTP endpoint (GET + POST)

The GraphQL layer is an independent module — remove it if you only need tRPC or REST. See docs/MODULES.md for removal instructions.

Server Actions

All mutations go through server actions in src/lib/actions/. Every action follows this pattern:

'use server';

import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { getAppSession } from '@/lib/auth';
import { checkRateLimit } from '@/lib/rate-limit';
import { updateProfileSchema } from '@/lib/validation';

export async function updateProfile(formData: FormData): Promise<void> {
  const session = await getAppSession();
  if (!session) redirect('/sign-in');

  const { allowed } = checkRateLimit(`settings:${session.user.id}`, {
    limit: 10,
    windowSeconds: 60,
  });
  if (!allowed) throw new Error('Too many requests. Please wait a moment.');

  const data = updateProfileSchema.parse({
    name: formData.get('name'),
  });

  // ... DB update ...

  revalidatePath('/dashboard/settings');
}

Available Actions

ActionFileDescription
createCheckoutbilling.tsCreate a Stripe/LS checkout session
openCustomerPortalbilling.tsOpen the billing portal
updateProfilesettings.tsUpdate user name and preferences
deleteAccountsettings.tsDelete the current user account
completeOnboardingonboarding.tsMark onboarding as complete
setLocalelocale.tsChange the user's locale
changeUserRoleadmin.tsAdmin: change a user's role
deleteUseradmin.tsAdmin: delete a user
createApiKeyActionapi-keys.tsGenerate a new API key
deleteApiKeyActionapi-keys.tsRevoke an API key

API Versioning

Codapult supports URL-prefix versioning via a proxy layer in src/proxy.ts.

How It Works

Requests to /api/v1/users are rewritten to /api/users internally. The version is extracted and tracked through the request lifecycle.

GET /api/v1/users  →  internally routed to  →  GET /api/users

Version Endpoint

GET /api/version returns the current API version status:

{
  "current": "v1",
  "supported": ["v1", "v2"],
  "versions": {
    "v1": { "deprecated": false },
    "v2": { "deprecated": false }
  }
}

Response Headers

Every versioned response includes:

HeaderDescription
X-API-VersionThe resolved API version
Deprecationtrue if the version is deprecated
SunsetISO date when the version will be removed
LinkURL of the successor version

Clients can send X-API-Version as a request header to opt into version-specific behavior.

Configuration

Version definitions live in src/lib/api-version.ts:

export const API_VERSIONS = ['v1', 'v2'] as const;
export const CURRENT_VERSION: ApiVersion = 'v1';

export const VERSION_INFO: Record<ApiVersion, { deprecated: boolean; sunset?: string }> = {
  v1: { deprecated: false },
  v2: { deprecated: false },
};

Rate Limiting

Codapult includes a sliding-window rate limiter in src/lib/rate-limit.ts.

Usage

import { checkRateLimit } from '@/lib/rate-limit';

const { allowed, remaining, resetAt } = checkRateLimit(`chat:${session.user.id}`, {
  limit: 30,
  windowSeconds: 60,
});

The checkRateLimit function returns:

FieldTypeDescription
allowedbooleanWhether the request is within limits
remainingnumberRemaining requests in the window
resetAtnumberTimestamp (ms) when the window resets

Where It's Applied

ScopeLimitWindow
Auth endpointsConfigured per route60s
AI chat30 requests60s
Billing actions5 requests60s
Admin actions10 requests60s
GraphQL60 requests60s
Plugin routes30 requests60s

Note: The built-in limiter is in-memory and not shared across serverless instances. For production, swap it with a Redis-backed implementation (e.g. Upstash Rate Limit).

Payments & BillingAI Features