Code Standards
TypeScript
- Strict mode is enabled in
tsconfig.json— no implicitany, no uncheckednull. - Use
import type { X }for type-only imports. - Keep domain types in
lib/domain/types.ts. Avoid duplicating type definitions across files. - Use
interfacefor object shapes that may be extended;typefor unions and aliases.
Components
Client vs server components
The project uses the Next.js App Router. Add "use client" to any component that:
- Uses React hooks (
useState,useEffect,useRef, etc.) - Attaches event handlers
- Uses browser APIs
Zustand selector discipline
Subscribe only to the slices you need to prevent unnecessary re-renders:
// ✓ Subscribe to a specific slice
const notes = useBoardStore((s) => s.notes)
// ✗ Subscribe to entire store — re-renders on any change
const store = useBoardStore()Naming conventions
| Item | Convention | Example |
|---|---|---|
| Component files | PascalCase | StickyNote.tsx |
| Utility files | camelCase | boardStore.ts |
| Constants | SCREAMING_SNAKE_CASE | CANVAS_WIDTH |
| Types and interfaces | PascalCase | Note, BoardState |
State mutations
All board state mutations must follow the optimistic update pattern:
// 1. Update local state immediately
set((s) => { s.notes[note.id] = note })
// 2. Push to server asynchronously
syncManager.createNote(note)Never wait for a server response before updating the UI.
API routes
Auth pattern
All board-scoped routes use boardGuard() as the first operation:
export async function GET(req: Request, { params }: { params: { boardId: string } }) {
const guard = await boardGuard(req, params.boardId)
if (guard instanceof NextResponse) return guard // 401 / 403 / 503
const { email, kv, boardAccess } = guard
// ... handler logic
}Never implement auth inline in a route handler.
Cloudflare context pattern
Use the getCloudflareContext() try/catch with a fail-closed production fallback:
try {
const { env } = await getCloudflareContext<CloudflareEnv>()
kv = env.AUTH_KV
} catch {
if (isDevMode()) {
// Dev-only fallback
return devResponse
}
// Fail closed in production
return NextResponse.json({ error: "Service temporarily unavailable" }, { status: 503 })
}Never bypass the catch block silently. Always use isDevMode() to gate dev fallbacks.
Input validation
Validate all API request bodies with Zod before processing:
const parsed = MySchema.safeParse(await req.json())
if (!parsed.success) {
return NextResponse.json({ error: "Invalid payload" }, { status: 400 })
}Error handling
- Return structured JSON error responses with appropriate status codes.
- Use
logError()for unexpected exceptions — neverconsole.errordirectly. - Never leak stack traces or internal system details in API responses.
Debouncing writes
Expensive persistence operations must be debounced:
- IDB saves: ≥300ms debounce
- D1 push for content changes: 5-second cloud debounce
- Changelog writes: 15-second per-note debounce
- Phase sync: 2-second debounce
Security requirements
- Never commit API keys, secrets, or bypass flags.
- Use
wrangler secret putfor all production secrets. - All AI endpoints must call
checkAiRateLimit()andcheckAiQuota()before processing. - OTP send and verify routes must call
checkOtpIpRateLimit().
See also
- Tech Stack — Libraries and frameworks used
- Deployment — How to deploy