tRPC layer
@repo/api is the shared tRPC layer — used by apps/svelte-web (server-side caller) and the native mobile apps (hand-written tRPC-over-HTTP client). Zero external SDK dependencies — only TypeScript interfaces; the host app injects concrete implementations.
Layout
Section titled “Layout”packages/api/src/├── index.ts # types + browser-safe transforms (pure type barrel)├── server.ts # runtime exports for hosts (appRouter, createCallerFactory)├── router.ts # merges 19 domain routers├── trpc.ts # initTRPC + procedure factories + rate-limit + observability seam├── routers/ # one file per domain (thin — delegate to services)├── services/ # business logic (framework-agnostic)├── types/│ ├── services.ts # Services interface (host implements)│ ├── search.ts # LocationDocument, AgentDocument│ └── property.ts # PropertyMarker, PropertyWithImage└── transforms/ └── property.ts # toNum, normalizePropertyDetailTwo entry points
Section titled “Two entry points”| Import | Use from |
|---|---|
@repo/api (root) | import type { AppRouter, Context, Services, ... } + pure transform functions. Safe to import from any code, including client bundles. |
@repo/api/server | import { appRouter, createCallerFactory }. Server-only — these value re-exports drag the router + service tree (including the @repo/database/kysely runtime + pg) into whatever bundle imports them. |
Routers (19)
Section titled “Routers (19)”addresses, admin, agents, ai, analytics, chat, dashboard, inventory, leads, listings, lists, locations, notifications, portal, properties, savedSearches, teams, tours, users.
Source of truth: packages/api/src/router.ts.
Services (18)
Section titled “Services (18)”Each router has a matching service in packages/api/src/services/. Two team-specific helpers are split out: team-analytics.ts, team-member-detail.ts. Plus listings-write.ts (split from listings.ts).
Procedure types
Section titled “Procedure types”From packages/api/src/trpc.ts:
| Procedure | Guard |
|---|---|
publicProcedure | No auth |
protectedProcedure | Requires ctx.user |
agentProcedure | Requires AGENT role |
rateLimit(scope, limit, windowSeconds) | Per-user middleware via ctx.services.rateLimit.byUser |
publicRateLimit(scope, limit, windowSeconds) | IP-based for public procedures |
Observability span. Every procedure also runs an observe base middleware first (outermost, so the span wraps auth + rate-limit). It hands off to a host-registered middleware via setMonitoringMiddleware(fn) (re-exported from @repo/api/server) — each host passes its own Sentry.trpcMiddleware({ attachRpcInput: true }) at startup. This keeps @repo/api free of any @sentry/* import (the package’s zero-SDK rule) and is a no-op when unregistered (e.g. tests). See Analytics & monitoring.
Context shape
Section titled “Context shape”Context is a pure object — no SvelteKit types allowed in this package. The host app builds it:
interface Context { user: { id, email, roles, name, image } | null; session: { id } | null; clientIp: string; db: Database; // Kysely handle from '@repo/database/kysely' services: Services;}The Services interface (see packages/api/src/types/services.ts) is the contract the host fulfils — rateLimit, search clients, S3, notifications, etc.
Adding a procedure
Section titled “Adding a procedure”Routers stay thin. Business logic in services.
export async function getLead(db: Database, userId: string, id: string) { // business logic here}
// packages/api/src/routers/leads.tsexport const leadsRouter = router({ get: protectedProcedure .use(rateLimit('leads.get', 60, 60)) .input(z.object({ id: z.string() })) .query(({ ctx, input }) => getLead(ctx.db, ctx.user.id, input.id)),});Web caller (server-side)
Section titled “Web caller (server-side)”import { appRouter, createCallerFactory } from '@repo/api/server'; // runtime subpathimport type { Context, Services } from '@repo/api'; // type barrel
const createCaller = createCallerFactory(appRouter);// build Context in hooks.server.ts, then: const api = createCaller(ctx);Mobile client (HTTP)
Section titled “Mobile client (HTTP)”The native apps speak the tRPC wire protocol with a hand-written thin client (TrpcClient.swift / TrpcClient.kt) — there is no @trpc/react-query and no compile-time AppRouter types. Queries are GET /api/trpc/{router.procedure}?input=…, mutations POST, SuperJSON envelope, no batching (one request per call; native concurrency handles parallelism), transport-failure-only retry. The response model layer is generated from @repo/contract. Full details on Conventions → Mobile (Native).
Hard rules
Section titled “Hard rules”- No SvelteKit imports in
@repo/api. NoRequestEvent,Cookies,Locals,$env/*. If you need request-y data, add it toContextorServicesand have the host inject it. - Routers stay thin. They validate input, apply middleware, and delegate to a service. Business logic in
src/services/. - superjson is the transformer —
Dateserializes/deserializes automatically. Postgres returnsnumericcolumns as strings (not aDecimaltype); convert withNumber(...)before doing math. - Enums — read
packages/database/src/enums.ts(values) +packages/database/src/generated/db.ts(column types) before writing inputs.Tour.status = REQUESTED(notPENDING).
Web mutations vs tRPC
Section titled “Web mutations vs tRPC”- Web mutations: SvelteKit form actions in
+page.server.ts. Permanent — do not migrate to tRPC. They give progressive enhancement, no-JS support, and superforms validation. - Web read queries / typed reads / multi-step flows: tRPC is fine.
- Mobile: tRPC for everything (mobile has no form actions).
- Shared business logic: put it in
src/services/and call from both the form action (via a thin wrapper) and the tRPC procedure.
See Conventions → Web for the form-action template.
Verification after adding a procedure
Section titled “Verification after adding a procedure”bun --filter @repo/api check-typesbun --filter svelte-web checkThe native mobile apps are plain directories (not bun-workspace members), so bun run check does not cover them — if a procedure’s response shape changes, update the @repo/contract schema and regenerate the native models (CI’s tests job enforces it). See Conventions → Mobile (Native).