API

Calling the API

The apps call the backend through a typed tRPC client. Here's how a call gets from a button to the database.

The client

Each app has one tRPC client, typed by the backend's AppRouter. In the web app it's web-app/src/trpc.ts:

import { trpc } from "./trpc";

const roles = await trpc.roles.list.query();
await trpc.roles.create.mutate({ name: "Support" });

Queries use .query(), and mutations use .mutate(). Inputs and results are typed from the server, so a wrong field name fails the build.

The client batches calls made in the same tick into one request. Queries with input are sent as POST, so a large input can't overflow a URL.

Where requests go

PathWhat handles it
/trpc/*Every tRPC procedure
/api/auth/*Better Auth — sign-in, sign-up, sessions
/chatbot/streamStreaming chatbot replies
/healthA plain health check

In development, the web app's Vite server proxies all four to localhost:3000, so requests are same-origin and the session cookie is sent automatically.

Auth on each request

  • Web app — the session cookie, sent with credentials: "include".
  • Desktop app — Authorization: Bearer <token>, since its webview can't share the browser's cookies.

The backend reads the session from the request headers and hands it to every procedure as ctx.session.

Errors

A failed call throws a TRPCClientError. Check its data.code:

CodeMeaning
UNAUTHORIZEDNo session
FORBIDDENSigned in, but missing the permission — or the terms aren't accepted
BAD_REQUESTInput failed validation, or a rule refused it
NOT_FOUNDThe thing doesn't exist, or you can't see it

Routers

The root router in backend/src/trpc/router.ts has 13 routers: health, db, userSettings, profile, admin, authProtocols, termsAndConditions, features, featureRoles, roles, organizations, chatbot, and blog.

Next