tRPC Router Foundation & Org-Scoped Context
tRPC Router Foundation & Org-Scoped Context
Introduced in: v1.0.7
This page describes the tRPC server setup and the base context contract that underpins every API procedure in the platform.
Overview
The platform uses tRPC to build its internal API layer. Rather than exposing REST or GraphQL endpoints, tRPC procedures are called directly from the front-end with full end-to-end type safety.
Version 1.0.7 establishes the server entry point and defines a base context that is resolved once per request and passed into every procedure automatically.
Base Context
Every tRPC procedure — regardless of where it sits in the router tree — receives the following context object:
interface Context {
session: Session | null; // Authenticated user session
org: Organization; // Resolved tenant organisation
db: DrizzleClient; // Drizzle ORM database client
}
session
The authenticated session for the incoming request. Resolved via the platform's SSO/auth layer before the procedure executes. Procedures that require authentication should check that session is non-null and reject unauthenticated calls.
org
The organisation record corresponding to the current tenant, resolved from the request context (e.g. subdomain, header, or session claim). All data queries should be scoped to this organisation to maintain strict multi-tenant isolation.
db
A ready-to-use Drizzle ORM client connected to the platform database. Procedures use this client directly for type-safe SQL queries — no additional setup required inside a procedure.
Building Procedures on Top of This Foundation
All router files and feature procedures introduced in future releases extend from the base router and automatically inherit the context described above. A typical procedure looks like:
export const exampleRouter = router({
getItem: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
// ctx.session — authenticated user
// ctx.org — current tenant
// ctx.db — Drizzle client
return ctx.db.query.items.findFirst({
where: and(
eq(items.id, input.id),
eq(items.orgId, ctx.org.id), // always scope to org
),
});
}),
});
Multi-Tenant Isolation
Because the resolved org is part of every context, procedures have no way to accidentally access cross-tenant data without explicitly bypassing the org scope. This is the primary guard against data leakage in a multi-tenant environment.
As a convention:
- Always filter database queries with
eq(<table>.orgId, ctx.org.id). - Procedures that intentionally operate across organisations (e.g. internal admin jobs) must use a separate, explicitly named router that opts out of org scoping.
Architecture Notes
- The tRPC server is the single API contract for all business logic. No ad-hoc API routes should be added outside of tRPC without a documented reason.
- The context is resolved once per request and is immutable within a procedure.
- The Drizzle client (
db) is passed in rather than imported as a singleton, making procedures straightforward to unit-test by substituting a mock client.