Layer conventions
The strict separation between entry points, business logic, and persistence in apps/auth — and where new code goes.
The apps/auth codebase keeps a strict separation between entry points, business logic, and
persistence. Follow these rules when adding code.
Server actions
Files in src/app/actions/ (*-actions.ts) start with "use server". Each exported action calls
onServerAction(async (ctx, getServices) => { … }) and only invokes service methods inside the
callback. No business logic, and no direct getCloudflareContext(), env, or DB access. For the
current session, call Auth.js auth() directly — there is no service for session.
"use server";
import { onServerAction } from "@/lib/context/on-server-action";
export async function getCurrentUser() {
return onServerAction(async (ctx, getServices) => {
const { userService } = getServices();
return userService.getCurrentUser(ctx);
});
}API route handlers
Handlers in src/app/api/ carry no business logic. Use withApiContext and delegate to
services. Routes own transport concerns: decode and shape/format-validate the raw request into a
typed command before calling a service (never hand a service a raw, unparsed request body), and apply
request-level abuse guards such as rate limiting at the edge. A service receives already-validated,
typed inputs and focuses on domain logic.
When a guard needs persistence (e.g. a rate-limit counter in D1), keep the storage in a small service/repository the route calls, but the decision to allow/deny and the HTTP response stay in the route.
Reference implementation
The DCR endpoint is the reference: api/register/route.ts parses via parse-request.ts, calls
DcrRateLimitService for the counter, and only then invokes DcrService.
Services
Everything in src/lib/services/ holds all business logic. Services receive RequestContext and
depend on repositories; they do not call getCloudflareContext() or touch raw D1. The DI registry
(getServices) wires them.
Repositories
Everything in src/lib/repositories/ does persistence only (D1). They receive the DB (from
context) in the constructor; no Cloudflare env in repository methods, and no business rules.
Where new code goes
- A new feature adds a service (plus a repository if it persists data); entry points only wire context and call services.
- A new client-state domain adds a reducer module under
src/context/— see State management.
The worker.ts OpenNext rule
Do not 'clean up' apps/auth/worker.ts
apps/auth/worker.ts opens with a //DO NOT REMOVE THE FOLLOWING COMMENT line and a @ts-ignore
on the import openNextWorker from "./.open-next/worker.js" line. OpenNext only generates
.open-next/worker.js during opennextjs-cloudflare build, so the module is absent before the
first build. @ts-ignore (not @ts-expect-error) is deliberate: once the file exists post-build,
@ts-expect-error would become an unused directive and fail tsc with TS2578. Do not delete these
lines or swap in a plain import to satisfy an unused-directive warning unless explicitly asked.