# 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:

```ts
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

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

#### Correct

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

#### Wrong

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

#### Correct

```ts
export const dynamic = "force-dynamic";

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