API Layer
tRPC routers and procedures, the request context, route handlers, and how the admin app wires them up.
Code: packages/nextjs-cms/src/api/
| Folder | Contents |
|---|---|
trpc/trpc.ts | initTRPC, context, publicProcedure, privateProcedure, pluginProcedure, SuperJSON transformer |
trpc/root.ts | coreRouters: the built-in routers |
trpc/routers/*.ts | One router per domain |
trpc/server.ts | createTRPCRouter(): merges core, plugin, and app routers; RSC helpers; fetch handler |
actions/*.ts | Server functions the routers call (pages.ts, privileges.ts, files.ts) |
client/ | React client provider for the admin |
plugin/ | The subset of tRPC helpers plugins may import |
Context and procedures
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth() // from the access_token cookie
return { db, session, ...opts }
}| Procedure | Guard |
|---|---|
publicProcedure | none |
privateProcedure | isAuthedMiddleware: throws UNAUTHORIZED without a session; narrows ctx.session to non-null |
pluginProcedure(name, op?) | isAuthed + a row in admin_privileges for name (and op in its operations). The dashboard-override plugin bypasses the privilege check. |
Section routers check privileges themselves through SectionFactory.getSectionForAdmin() and isAccessAllowed(), returning NOT_FOUND (not FORBIDDEN) so that unauthorized admins can't discover which sections exist.
Core routers
| Key | Procedures | Backed by |
|---|---|---|
auth | login, logout | auth/lib/actions.ts |
admins | list, get, create, update, remove | actions/privileges.ts |
accountSettings | get, save, changePassword, updateLanguage | |
config | getLocalization | getCMSConfig() |
navigation | getSidebar | actions/pages.ts#getSidebar |
hasItemsSections | listItems, newItem, editItem, deleteItem, deleteLocaleTranslation | actions/pages.ts |
simpleSections | create, deleteLocaleTranslation | actions/pages.ts |
categorySections | get, getChildren, deleteItem | actions/pages.ts |
fields | tagsAutoComplete | |
files | getPhoto | actions/files.ts |
gallery | deletePhoto | |
logs | list, recent, filterOptions | logging |
plugins | checkRoute | plugins/loader.ts |
Reads go through tRPC. Writes of form data (create/edit with files) go through route handlers under apps/cms/app/api/submit/, because they're multipart FormData handled by the submit classes.
createTRPCRouter()
The admin creates one instance:
import 'server-only'
import { createTRPCRouter } from 'nextjs-cms/api/server'
export const trpc = createTRPCRouter()
export type AppRouter = typeof trpc.appRouter
export const { api, HydrateClient } = trpcIt accepts app-owned routers too (createTRPCRouter({ routers: { myFeature } })); keys that collide with core routers throw at startup.
At request time, getMergedRouter() builds { ...coreRouters, ...pluginRouters, ...userRouters }, loading plugins with the plugin loader. The result is cached per config version, so editing cms.config.ts (adding a plugin) takes effect without a restart. Plugin router keys that collide with core or app routers throw.
The instance exposes:
handler:{ GET, POST }forapp/api/trpc/[trpc]/route.ts.api: an async server caller for RSC (await api.navigation.getSidebar()), with React Query hydration helpers.HydrateClient: wraps client trees that use prefetched data.
Server-only boundaries
api/server starts with import 'server-only'. Anything that plugins need (and that must be importable in contexts where server-only would break, such as jiti-loaded plugin code) is re-exported from api/plugin instead. Keep that split when adding exports.
Adding a procedure
- Put the logic in
actions/(testable without tRPC). - Add the procedure to the relevant router, with a Zod
input. - Pick the guard:
privateProcedureplus an explicit privilege check for section data, orpluginProcedurefor plugin data. - Translate user-facing errors with
getString(key, ctx.session.user.language). - Add a router test next to the router (
routers/*.test.ts).