eetr-auth
Contributing

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.

On this page