Documentation
Contributing
Code Standards

Code Standards

TypeScript

  • Strict mode is enabled in tsconfig.json — no implicit any, no unchecked null.
  • Use import type { X } for type-only imports.
  • Keep domain types in lib/domain/types.ts. Avoid duplicating type definitions across files.
  • Use interface for object shapes that may be extended; type for 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

ItemConventionExample
Component filesPascalCaseStickyNote.tsx
Utility filescamelCaseboardStore.ts
ConstantsSCREAMING_SNAKE_CASECANVAS_WIDTH
Types and interfacesPascalCaseNote, 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 — never console.error directly.
  • 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 put for all production secrets.
  • All AI endpoints must call checkAiRateLimit() and checkAiQuota() before processing.
  • OTP send and verify routes must call checkOtpIpRateLimit().

See also