Skip to main content
The backend API is built with tRPC, providing end-to-end type safety from server to client. Every procedure is a typed function with input validation, access control, and metadata — making the API self-documenting and available as an AI agent tool.

Task routes

The task tracker needs routes to create, list, and update tasks. Create src/server/routes/tasks.ts:
import { router, scopedProcedure } from '@synthetiq/app-framework/server';
import { z } from 'zod';

export const tasksRouter = router({
  getMyTasks: scopedProcedure([])
    .meta({ description: "Get current user's tasks" })
    .query(async ({ ctx }) => {
      return ctx.db.task.findMany({
        where: { userId: ctx.userId },
        orderBy: { createdAt: 'desc' },
      });
    }),

  getAllTasks: scopedProcedure(['tasks:viewAll'])
    .meta({ description: 'Get all tasks across all users (admin)' })
    .query(async ({ ctx }) => {
      return ctx.db.task.findMany({
        include: { user: { select: { id: true, name: true } } },
        orderBy: { createdAt: 'desc' },
      });
    }),

  createTask: scopedProcedure([])
    .meta({ description: 'Create a new task' })
    .input(z.object({
      title: z.string().min(1),
      priority: z.number().min(1).max(5).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.task.create({
        data: { title: input.title, priority: input.priority, userId: ctx.userId },
      });
    }),

  updateTask: scopedProcedure([])
    .meta({ description: 'Update a task' })
    .input(z.object({
      id: z.string(),
      title: z.string().min(1).optional(),
      status: z.enum(['active', 'completed', 'archived']).optional(),
      priority: z.number().min(1).max(5).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.task.update({
        where: { id: input.id, userId: ctx.userId },
        data: input,
      });
    }),
});
Key points:
  • scopedProcedure([]) requires authentication — any logged-in user can call the procedure
  • scopedProcedure(['tasks:viewAll']) additionally requires the tasks:viewAll scope
  • Every procedure must include .meta({ description }) — the build fails without it. The description is used by the AI agent, HTTP API docs, MCP server, and built-in docs pages.
  • Input is validated with Zod — invalid input returns a 400 error before the handler executes
  • ctx.userId identifies the authenticated user — never accept user identity as input

Registering routers

Register the new tasksRouter with the app in src/server/router.ts:
// src/server/router.ts
import { setAppRouter } from "./init";
import { router, adminRouter, userRouter, utilsRouter, aiAgentRouter,
         docsRouter, servicesRouter, oauthAdminRouter, oauthAppsRouter,
         workflowAdminRouter, logsRouter, metricsRouter } from "@synthetiq/app-framework/server";
import { tasksRouter } from "./routes/tasks";

export const appRouter = router({
  utils: utilsRouter,
  user: userRouter,
  ai: aiAgentRouter,
  admin: adminRouter,
  docs: docsRouter,
  services: servicesRouter,
  oauthAdmin: oauthAdminRouter,
  oauthApps: oauthAppsRouter,
  workflowAdmin: workflowAdminRouter,
  logs: logsRouter,
  metrics: metricsRouter,
  tasks: tasksRouter,
});

setAppRouter(appRouter);

export type AppRouter = typeof appRouter;
Always include all system routers. The built-in admin pages, docs pages, and monitoring pages depend on them.
For the full context object, procedure type variants, streaming procedures, and MCP exclusion, see the Procedures reference.