Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Procedures

Creating tRPC procedures

Procedure Types

Different procedure types provide different levels of authentication and authorization:

ProcedureDescriptionUse Case
publicProcedureNo authentication requiredPublic data, health checks
authProcedureRequires valid sessionUser-specific operations
roleProcedure(role)Requires specific roleAdmin operations

Public Procedure

No authentication required:

import { publicProcedure } from "@/trpc/procedures/trpc";

export const healthRouter = createTRPCRouter({
  ping: publicProcedure.query(() => {
    return { status: "ok" };
  }),
});

Auth Procedure

Requires a valid session. The user is automatically available in ctx.user:

import { authProcedure } from "@/trpc/procedures/trpc";

export const profileRouter = createTRPCRouter({
  getProfile: authProcedure.query(async ({ ctx }) => {
    // ctx.user is guaranteed to be non-null
    const userId = ctx.user.id;
    return await ctx.db.users.get({ id: userId });
  }),
});

Role Procedure

Requires a specific role or roles:

import { roleProcedure } from "@/trpc/procedures/trpc";
import { UserRole } from "@/db/enums";

export const adminRouter = createTRPCRouter({
  // Single role
  listUsers: roleProcedure(UserRole.ADMIN).query(async ({ ctx }) => {
    return ctx.db.users.list();
  }),

  // Multiple roles allowed
  moderateContent: roleProcedure([UserRole.ADMIN, UserRole.MODERATOR])
    .mutation(async ({ ctx }) => {
      // ...
    }),
});

Rate Limiting

Rate limiting is configured per procedure via .meta():

list: authProcedure
  .meta({ rateLimit: "QUERY" })
  .query(async ({ ctx }) => { ... })

Rate Limit Keys

Rate Limit KeyUse Case
"QUERY"Read operations
"MUTATION"Write operations
"AUTH"Authentication operations
falseDisable rate limiting

Configure limits in src/config/app.ts under Config.RateLimit.

ActionResponse Pattern

All procedures should return ActionResponse for consistent error handling:

import type { ActionResponse } from "@/types/action-response";

export const myRouter = createTRPCRouter({
  create: authProcedure
    .input(createSchema)
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.myResource.create(input);

      return {
        success: true,
        message: ctx.t("toasts.saved"),
        payload: result,
      } satisfies ActionResponse;
    }),
});

Error Handling

For errors that can be recovered, return ActionResponse:

return {
  success: false,
  message: ctx.t("customErrors.organization.invitation-invalid"),
} satisfies ActionResponse;

If execution cannot continue, throw a CustomError:

import { CustomError } from "@/lib/errors";

throw new CustomError("customErrors.generic.error");

CustomError keys are in src/messages/dictionaries/<locale>/customErrors.json. See Error Handling for more info.

Creating a Procedure

Basic Example

import { authProcedure } from "@/trpc/procedures/trpc";
import { z } from "zod";

export const myRouter = createTRPCRouter({
  list: authProcedure
    .meta({ rateLimit: "QUERY" })
    .query(async ({ ctx }) => {
      const data = await ctx.db.myResource.list({
        where: { userId: ctx.user.id },
      });

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

  create: authProcedure
    .meta({ rateLimit: "MUTATION" })
    .input(z.object({
      name: z.string().min(1),
      description: z.string().optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const result = await ctx.db.myResource.create({
        ...input,
        userId: ctx.user.id,
      });

      return {
        success: true,
        message: ctx.t("toasts.saved"),
        payload: result,
      } satisfies ActionResponse;
    }),
});

Next Steps

Creating Routers

New tRPC Router Template

On this page

Procedure Types
Public Procedure
Auth Procedure
Role Procedure
Rate Limiting
Rate Limit Keys
ActionResponse Pattern
Error Handling
Creating a Procedure
Basic Example
Next Steps