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

Quick Start

Create your first feature and understand the development workflow

Your First Feature

Let's create a simple "tasks" feature to understand the development workflow.

Step 1: Create the Feature Directory

mkdir -p src/features/tasks

Step 2: Define the Schema

Create src/features/tasks/schema.ts:

import { baseTableSchema } from "@/db/enums";
import { z } from "zod";

export const MIN_TASK_TITLE = 3;
export const MAX_TASK_TITLE = 100;

// Main schema
export const taskSchema = baseTableSchema.extend({
  title: z.string().min(MIN_TASK_TITLE).max(MAX_TASK_TITLE),
  description: z.string().optional(),
  completed: z.boolean().default(false),
  ownerId: z.string().uuid(),
});

// Upsert schema
export const taskUpsertSchema = taskSchema
  .omit({ createdAt: true, updatedAt: true })
  .partial({ id: true });

// Types
export type Task = z.infer<typeof taskSchema>;
export type TaskUpsert = z.infer<typeof taskUpsertSchema>;

Step 3: Create the Database Table

Create src/db/tables/tasks.ts:

import { createTable } from "@/db/table-utils";
import { relations } from "drizzle-orm";
import { boolean, text, uuid } from "drizzle-orm/pg-core";
import { users } from "./auth";

export const tasks = createTable("tasks", {
  title: text("title").notNull(),
  description: text("description"),
  completed: boolean("completed").notNull().default(false),
  ownerId: uuid("owner_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
});

export const tasksRelations = relations(tasks, ({ one }) => ({
  owner: one(users, {
    fields: [tasks.ownerId],
    references: [users.id],
  }),
}));

Export in src/db/tables/index.ts:

export * from "./tasks";

Push to database:

npm run db:push

Step 4: Create Functions

Create src/features/tasks/functions.ts:

import { createDrizzleOperations } from "@/db/drizzle-operations";
import { CommonTableData } from "@/db/enums";
import { tasks } from "@/db/tables";
import { Task } from "./schema";
import { eq } from "drizzle-orm";

type DataCore = Omit<Task, keyof CommonTableData>;

const operations = createDrizzleOperations<typeof tasks, Task>({
  table: tasks,
});

export async function list() {
  return operations.listDocuments();
}

export async function get(id: string) {
  return operations.getDocument(id);
}

export async function create(data: DataCore) {
  return operations.createDocument(data);
}

export async function update(id: string, data: Partial<DataCore>) {
  return operations.updateDocument(id, data);
}

export async function remove(id: string) {
  return operations.removeDocument(id);
}

export async function getByOwnerId(ownerId: string) {
  return operations.listDocuments(eq(tasks.ownerId, ownerId));
}

Step 5: Add to Database Facade

Add to src/db/facade.ts:

import * as tasks from "@/features/tasks/functions";

export const dbFacade = {
  // ... existing
  tasks,
};

Step 6: Create tRPC Router

Create src/trpc/procedures/routers/tasks.ts:

import { taskUpsertSchema } from "@/features/tasks/schema";
import { authProcedure, createTRPCRouter } from "@/trpc/procedures/trpc";
import { ActionResponse } from "@/types";
import { z } from "zod";

export const tasksRouter = createTRPCRouter({
  list: authProcedure
    .meta({ rateLimit: "QUERY" })
    .input(z.object({}))
    .query(async ({ ctx }) => {
      const tasks = await ctx.db.tasks.getByOwnerId(ctx.user.id);
      return {
        success: true,
        payload: tasks,
      } satisfies ActionResponse;
    }),

  upsert: authProcedure
    .meta({ rateLimit: "MUTATION" })
    .input(taskUpsertSchema)
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input;
      
      const task = id
        ? await ctx.db.tasks.update(id, data)
        : await ctx.db.tasks.create({ ...data, ownerId: ctx.user.id });

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

  delete: authProcedure
    .meta({ rateLimit: "MUTATION" })
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.db.tasks.remove(input.id);
      return {
        success: true,
        message: ctx.t("toasts.deleted"),
      } satisfies ActionResponse;
    }),
});

Add to src/trpc/procedures/root.ts:

import { tasksRouter } from "./routers/tasks";

export const appRouter = createTRPCRouter({
  // ... existing
  tasks: tasksRouter,
});

Step 7: Create Client Hooks

Create src/features/tasks/hooks.ts:

"use client";

import { api } from "@/trpc/react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { taskUpsertSchema, TaskUpsert } from "./schema";

export function useTasksQuery() {
  const query = api.tasks.list.useQuery({});
  return {
    tasks: query.data?.payload ?? [],
    loading: query.isLoading,
    refetch: query.refetch,
  };
}

export function useTaskForm(defaultValues: TaskUpsert | null) {
  const utils = api.useUtils();
  const form = useForm<TaskUpsert>({
    resolver: zodResolver(taskUpsertSchema),
    defaultValues: defaultValues ?? { title: "", completed: false },
  });

  const mutation = api.tasks.upsert.useMutation({
    onSuccess: () => {
      utils.tasks.list.invalidate();
    },
  });

  const save = form.handleSubmit(async (data) => {
    await mutation.mutateAsync(data);
  });

  return {
    form,
    save,
    loading: mutation.isPending,
  };
}

export function useTaskDelete() {
  const utils = api.useUtils();
  const mutation = api.tasks.delete.useMutation({
    onSuccess: () => {
      utils.tasks.list.invalidate();
    },
  });

  return {
    deleteTask: (id: string) => mutation.mutateAsync({ id }),
    loading: mutation.isPending,
  };
}

Step 8: Create a Simple Component

Create src/features/tasks/components/task-list.tsx:

"use client";

import { Button } from "@/components/ui/button";
import { Icon } from "@/lib/icons";
import { useTasksQuery, useTaskDelete } from "../hooks";

export function TaskList() {
  const { tasks, loading } = useTasksQuery();
  const { deleteTask } = useTaskDelete();

  if (loading) return <div>Loading...</div>;

  return (
    <div className="space-y-2">
      {tasks.map((task) => (
        <div key={task.id} className="flex items-center gap-2 p-4 border rounded">
          <span>{task.title}</span>
          <Button
            size="sm"
            variant="destructive"
            onClick={() => deleteTask(task.id)}
          >
            <Icon iconKey="trash" />
          </Button>
        </div>
      ))}
    </div>
  );
}

Testing Your Feature

  1. Start the dev server: npm run dev
  2. Import and use <TaskList /> in any page
  3. The tasks will be filtered by the current user automatically

What You Learned

  • Schema definition with Zod
  • Database table creation with Drizzle
  • Server functions using createDrizzleOperations
  • tRPC router with type-safe procedures
  • Client hooks for queries and mutations
  • Component integration with React Query

Next Steps

For more complex features with forms, dialogs, and field components:

  • Feature Modules - Complete 5-file pattern
  • Templates - Step-by-step feature creation guide
  • Database Operations - Advanced queries and caching

On this page

Your First Feature
Step 1: Create the Feature Directory
Step 2: Define the Schema
Step 3: Create the Database Table
Step 4: Create Functions
Step 5: Add to Database Facade
Step 6: Create tRPC Router
Step 7: Create Client Hooks
Step 8: Create a Simple Component
Testing Your Feature
What You Learned
Next Steps