Documentation
Documentation
Introduction

Getting Started

Getting started
Getting StartedInstallationQuick StartProject Structure

Configuration

Configuration
ConfigurationEnvironment ConfigurationEdge ConfigDatabaseAuth SecretStripeFirebaseStorageGoogle Maps And Cloud Service AccountOAuth ProvidersEmail DeliverySentryFeature Flags

Architecture

Architecture
Architecture OverviewTech StackoRPC MiddlewareDesign Principles

Patterns

Patterns
Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

Database
DatabaseSetupSchema DefinitionDatabase OperationsMigrationsCaching
Data Tables

API

oRPCProceduresRoutersoRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

Storage
StorageConfigurationUsageBuckets
Stripe Billing

Extra

Caching

Templates

Templates
Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate oRPC RouterAdd Translations

Development

Development
DevelopmentCommandsAI AgentsBest Practices
Pulling Updates

Procedures

Creating oRPC procedures

Procedure Types

Import from @/rpc/procedures/rpc:

import {
  authProcedure,
  createRPCRouter,
  publicProcedure,
  roleProcedure,
} from "@/rpc/procedures/rpc";
ProcedureDescription
publicProcedureNo authentication required
authProcedureRequires an authenticated user
roleProcedure(role)Requires one or more roles

Basic Patterns

Public procedure

export const healthRouter = createRPCRouter({
  ping: publicProcedure
    .meta({ rateLimit: false })
    .query(async () => ({ success: true, payload: { status: "ok" } })),
});

Authenticated procedure

import { type ActionResponse } from "@/lib/utils/schema-utils";

export const todosRouter = createRPCRouter({
  list: authProcedure
    .meta({ rateLimit: "QUERY" })
    .query(async ({ ctx }) => {
      const items = await ctx.db.todos.getByOrganizationId("org-id");

      return {
        success: true,
        payload: items,
      } satisfies ActionResponse;
    }),
});

Role-based procedure

import { UserRole } from "@/db/enums";

export const adminRouter = createRPCRouter({
  stats: roleProcedure(UserRole.ADMIN)
    .meta({ rateLimit: "ADMIN" })
    .query(async () => ({ success: true })),
});

Rate Limits

Rate limits are configured via .meta({ rateLimit: ... }).

Supported keys in this project include:

  • "QUERY"
  • "MUTATION"
  • "AUTH"
  • "ADMIN"
  • false (disable)

OpenAPI Exposure

Expose a procedure through REST using .route(...) and explicit schemas:

import { actionResponseSchema } from "@/lib/utils/schema-utils";
import { z } from "zod";

list: authProcedure
  .meta({ rateLimit: "QUERY", acceptApiKey: true })
  .route({
    method: "GET",
    path: "/todos/list",
    tags: ["todos"],
    summary: "List todos",
  })
  .input(z.object({ organizationId: z.string().uuid() }))
  .output(actionResponseSchema(z.array(z.object({ id: z.string() }))))
  .query(async ({ ctx, input }) => {
    const items = await ctx.db.todos.getByOrganizationId(input.organizationId);
    return { success: true, payload: items };
  });

Error Handling

Use ActionResponse for expected failures and ORPCError for hard failures:

import { ORPCError } from "@/rpc/procedures/rpc";

if (!canAccess) {
  throw new ORPCError("FORBIDDEN", { message: "Not allowed" });
}

See Error Handling for the full project pattern.

Next Steps

Routers

OpenAPI

Router Template

On this page

Procedure Types
Basic Patterns
Public procedure
Authenticated procedure
Role-based procedure
Rate Limits
OpenAPI Exposure
Error Handling
Next Steps