一键导入
backoffice
Adding, modifying, or guarding staff-only `/backoffice` features — cross-organisation admin tools gated behind `users.role === "admin"`.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Adding, modifying, or guarding staff-only `/backoffice` features — cross-organisation admin tools gated behind `users.role === "admin"`.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Adding or changing routes in `apps/api`. One source of truth (`defineApiEndpoint` + a Zod schema) becomes an HTTP endpoint, an OpenAPI operation, an MCP tool, and a TS SDK method — descriptions and contracts must be written with all four readers in mind.
Layering and boundaries, web vs public API, app layout (clients, routes, logging), ports/adapters, runtime-portable domain/shared/utils code, multi-tenancy, DDD layout, or anti-patterns.
Preparing a production release, pushing a vX.Y.Z release tag, running scripts/release.sh, or updating CHANGELOG.md with the changes that are about to be deployed to production.
Enables or disables Latitude production maintenance mode by redirecting all publicly exposed production services to the Better Stack status page. Use when asked to start, stop, toggle, verify, or prepare a maintenance window.
Adding or reading env vars, updating .env.example, or validating config at startup with parseEnv / parseEnvOptional.
ClickHouse queries, Goose migrations, chdb test schema, or telemetry storage paths.
| name | backoffice |
| description | Adding, modifying, or guarding staff-only `/backoffice` features — cross-organisation admin tools gated behind `users.role === "admin"`. |
When to use: Adding, modifying, or guarding staff-only /backoffice features — cross-organisation admin tools gated behind users.role === "admin". The backoffice is where platform staff reproduce customer-reported bugs, spot-check data across tenants, and (future) impersonate users for support.
Non-admin users — authenticated or not — MUST NOT be able to access, enumerate, or fingerprint the backoffice surface. Every response is indistinguishable from hitting a random 404. This is enforced by three independent guards; every guard is ship-blocking on its own.
apps/web/src/routes/backoffice/route.tsx asserts user.role === "admin" in beforeLoad and loader, throwing notFound() (not redirect or 403 — those leak the path) on failure. TanStack Start code-splitting means non-admins never fetch the backoffice chunk.
Every backoffice createServerFn handler MUST attach adminMiddleware from apps/web/src/server/admin-middleware.ts. The middleware fetches the session with Better Auth's cookie cache bypassed (so DB-level role demotions take effect on the next request, not 5 minutes later), rejects non-admins with NotFoundError (not 401/403 — the error shape must not fingerprint the admin surface), and injects context.adminUserId + context.user so handlers have admin identity available without re-fetching.
export const adminThing = createServerFn({ method: "GET" })
.middleware([adminMiddleware]) // GUARD, before input validation
.inputValidator(inputSchema)
.handler(async ({ data, context }): Promise<ThingDto> => {
const client = getAdminPostgresClient()
const result = await Effect.runPromise(
thingUseCase(data).pipe(
withPostgres(ThingRepositoryLive, client), // org defaults to "system" → RLS off
withTracing,
),
)
return toDto(result)
})
Middleware runs before inputValidator, so abusive payloads get rejected one step earlier (no Zod parse overhead on non-admin probes).
The sole exception is stopImpersonating, which uses impersonatingMiddleware (from the same file) instead. During an active impersonation the current session's user.role is the target's role (usually "user"), so an admin-role check would reject the very call the admin needs to exit impersonation. impersonatingMiddleware gates on session.impersonatedBy being set and injects both context.adminUserId (recovered before Better Auth swaps the cookie back) and context.targetUserId for the audit event.
Do not wrap createServerFn in a factory (e.g. createBackofficeServerFn = (opts) => createServerFn(opts).middleware([...])). TanStack Start's Vite plugin detects server functions by pattern-matching the literal createServerFn(...).handler(inlineFn) chain at the call site — a factory hides those tokens behind a different name, the compiler skips the file, and Node-only module-level imports (withTracing, getAdminPostgresClient, …) leak into the browser bundle, breaking pnpm build with MISSING_EXPORT errors against @repo/observability/browser.ts. Keep createServerFn literal at every call site and attach the middleware there; the .middleware(…) method is part of the chain the compiler recognises. Attaching at each call site also keeps the "which guard does this endpoint use?" decision visible in the handler body — important because stopImpersonating uses a different middleware than the rest.
The route loader in routes/backoffice/route.tsx cannot use createServerFn middleware (route loaders aren't server functions). It calls requireAdminSession() from admin-auth.ts instead — same underlying fresh-session + role check, just exposed as a plain async helper. Both helpers share assertAdminUser and getFreshSession.
Admin queries run through getAdminPostgresClient() (apps/web/src/server/clients.ts), a pool on the separate LAT_ADMIN_DATABASE_URL superuser secret. withPostgres defaults the organisation scope to OrganizationId("system"), which is the only sanctioned signal to skip the RLS set_config('app.current_organization_id', …) call (see packages/platform/db-postgres/src/sql-client.ts). Admin handlers have no organisation context — passing one into withPostgres from a backoffice handler is a bug.
@domain/admin
src/
<feature>/ # one folder per feature (search, users, ...)
*-result.ts | entity.ts # Zod schemas + types
*-repository.ts # port (class … extends ServiceMap.Service<…>)
*.ts # use-case(s)
*.test.ts # pure use-case tests with fake ports
index.ts # feature barrel
index.ts # re-exports every feature
Keep @domain/admin as one package with feature folders — do not split into @domain/admin-search, @domain/admin-users, etc. Features share enough scaffolding (policy, audit, RLS-bypass) that splitting causes churn without benefit.
Web-app per-feature split mirrors the package:
apps/web/src/domains/admin/
<feature>.functions.ts # createServerFn handler(s) + DTOs (guard = first line)
<feature>.functions.test.ts # input-schema tests
Routes live at apps/web/src/routes/backoffice/<feature>/ (using route.tsx / index.tsx, not _layout.tsx — the _ prefix contributes no URL segment and would collide with _authenticated/index.tsx on /).
Admin repository adapters (e.g. AdminSearchRepositoryLive in @platform/db-postgres) run queries without an organization_id filter. This is only safe because the admin client + "system" scope bypasses RLS. Every admin adapter source file carries a header warning explaining the wiring contract — copy that pattern when adding new adapters.
NotFoundError, not UnauthorizedError, for every admin guard failure.users.role is the global platform-staff flag ("user" | "admin"). DBA-only (Better Auth additionalFields.role declares input: false).members.role is per-organisation ("owner" | "admin" | "member"). Different concept. A user who is members.role = "admin" of their own org has zero backoffice access.@domain/admin): pure functions + fake ports via Layer.succeed(Port, stubImpl). No DB.@platform/db-postgres): PGlite via setupTestPostgres(); drive through withPostgres(Live, pg.adminPostgresClient) to match production admin wiring.apps/web/src/server/admin-auth.test.ts): cover admin / user / null / undefined / missing-role, and assert the error shape does not fingerprint the admin surface.admin-auth.test.ts — don't reassert it per handler.