Files
teatea-pension/.trellis/spec/backend/local-json-mvp.md

4.1 KiB

Local JSON MVP Persistence and API Contracts

Scenario: Single-Repo MVP Before Database Adoption

1. Scope / Trigger

  • Trigger: implementing full-stack CRUD, auth, RBAC, or audit logging before Drizzle/Postgres/oRPC/better-auth are installed.
  • Applies to this single-package Next.js app when the feature must be real and durable in local/runtime execution, but production database infrastructure is not yet available.
  • Store boundary: modules/core/server/store.ts.

2. Signatures

  • readData(): Promise<AppData>
  • writeData(data: AppData): Promise<void>
  • updateData<T>(mutator: (data: AppData) => T): Promise<T>
  • getCurrentAuthContext(): Promise<AuthContext | null>
  • requirePermission(permission: Permission, auditContext: DeniedAuditContext): Promise<PermissionCheckSuccess | PermissionCheckFailure>
  • Route Handlers use standard GET, POST, PATCH, and DELETE exports and return Response.

3. Contracts

  • Default data file: .data/teatea.json.
  • Override key: TEATEA_DATA_DIR points to the directory containing teatea.json.
  • .data/ must stay ignored; it may contain account hashes and session IDs.
  • Routes or layouts that read local JSON state or session cookies must opt out of static prerendering with export const dynamic = "force-dynamic".
  • API JSON responses backed by local JSON state or session cookies must include Cache-Control: no-store.
  • API response shape:
type ApiResult<T extends Record<string, unknown>> =
  | ({ success: true; reason: string } & T)
  | { success: false; reason: string };
  • Session cookie:
    • Name: teatea_session
    • Flags: httpOnly, sameSite: "lax", path: "/"
    • Lifetime: 7 days
  • Passwords:
    • Never persist plaintext.
    • Use Node crypto salt + scrypt hash until a dedicated auth library is introduced.

4. Validation & Error Matrix

  • Missing/expired session -> 401 with { success: false, reason: "未登录或会话已过期" }.
  • Missing permission -> 403 with { success: false, reason: "权限不足" } and a denied audit log.
  • Invalid JSON body -> 400 with a Chinese user-facing reason.
  • Missing record by ID -> 404 with { success: false, reason: "<entity>不存在" }.
  • Duplicate account email -> 409 with { success: false, reason: "账号已存在" }.

5. Good/Base/Bad Cases

  • Good: UI submits to Route Handler, Route Handler validates input, calls requirePermission, mutates data through updateData, writes audit log, returns ApiResult.
  • Base: Server Component reads data directly via readData after session/permission checks.
  • Bad: UI or route handler reads .data/teatea.json directly, bypasses requirePermission, or stores auth state in localStorage.
  • Bad: /app/*, /login, or /setup is prerendered as static content and caches a redirect based on build-time empty data.

6. Tests Required

  • pnpm lint
  • pnpm type-check
  • pnpm build
  • Manual or automated integration assertions:
    • first setup creates admin and cookie session
    • protected app route redirects without cookie
    • protected app route renders with a valid cookie after setup and does not return a cached /setup redirect
    • auth/bootstrap/session API responses include Cache-Control: no-store
    • CRUD mutation persists across reload/API list
    • denied permission returns 403 and writes an audit log

7. Wrong vs Correct

Wrong

window.localStorage.setItem("teatea.session", JSON.stringify(session));

Correct

const cookieStore = await cookies();
cookieStore.set({
  name: "teatea_session",
  value: sessionId,
  httpOnly: true,
  sameSite: "lax",
  path: "/",
});

Wrong

export default async function AppLayout({ children }: AppLayoutProps) {
  const bootstrap = await getBootstrapState();
  if (bootstrap.setupRequired) {
    redirect("/setup");
  }
  return children;
}

Correct

export const dynamic = "force-dynamic";

export default async function AppLayout({ children }: AppLayoutProps) {
  const bootstrap = await getBootstrapState();
  if (bootstrap.setupRequired) {
    redirect("/setup");
  }
  return children;
}