4.1 KiB
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, andDELETEexports and returnResponse.
3. Contracts
- Default data file:
.data/teatea.json. - Override key:
TEATEA_DATA_DIRpoints to the directory containingteatea.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
- Name:
- Passwords:
- Never persist plaintext.
- Use Node crypto salt + scrypt hash until a dedicated auth library is introduced.
4. Validation & Error Matrix
- Missing/expired session ->
401with{ success: false, reason: "未登录或会话已过期" }. - Missing permission ->
403with{ success: false, reason: "权限不足" }and a denied audit log. - Invalid JSON body ->
400with a Chinese user-facingreason. - Missing record by ID ->
404with{ success: false, reason: "<entity>不存在" }. - Duplicate account email ->
409with{ success: false, reason: "账号已存在" }.
5. Good/Base/Bad Cases
- Good: UI submits to Route Handler, Route Handler validates input, calls
requirePermission, mutates data throughupdateData, writes audit log, returnsApiResult. - Base: Server Component reads data directly via
readDataafter session/permission checks. - Bad: UI or route handler reads
.data/teatea.jsondirectly, bypassesrequirePermission, or stores auth state in localStorage. - Bad:
/app/*,/login, or/setupis prerendered as static content and caches a redirect based on build-time empty data.
6. Tests Required
pnpm lintpnpm type-checkpnpm 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
/setupredirect - 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;
}