Files
SnapOtter/CLAUDE.md
T
SnapOtter 51bc2d5732 fix: QA sweep fixes across migration, security, lint, and e2e tests
- fix(db): migration 0012 column order mismatch causing NOT NULL
  constraint failure on existing databases; use explicit column
  mapping instead of SELECT *
- fix(db): disable FK checks during migrations to allow SQLite
  table-recreation pattern (DROP + RENAME)
- fix(security): filter cookie_secret and instance_id from settings
  API response for non-admin users
- fix(lint): resolve all 7 API lint warnings (noParameterAssign,
  noImplicitAnyLet) in compose, image-enhancement, and workspace
- fix(docs): correct permission count from 16 to 14 in CLAUDE.md
- fix(e2e): resolve 44 Playwright test failures across 8 spec files
  including locator specificity, compress mode defaults, format count,
  restore-photo UI drift, stitch image count, GIF animated fixtures,
  submit button timing, and processing timeouts
2026-05-15 22:41:22 +08:00

9.7 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What Is SnapOtter

Open-source, self-hostable image manipulation suite (51 tools). Single Docker container, no external services. Dual-licensed AGPLv3 / commercial.

Monorepo Layout

apps/web/         # Main React SPA (port 1349)
apps/api/         # Fastify API server (port 13490)
apps/landing/     # Marketing site (Vite + React, deployed to Cloudflare Pages)
apps/docs/        # VitePress documentation site (deployed to Cloudflare Pages)
packages/shared/  # Constants, types, i18n, permissions -- consumed by all apps
packages/image-engine/  # Sharp-based image processing pipeline
packages/ai/      # Python sidecar bridge for ML models

Tech Stack

Layer Technology
Frontend React 19, Vite 6, Tailwind CSS 4, Zustand, react-router-dom v7
Backend Fastify 5, tsx (no compile step in dev), Sharp
Database SQLite via Drizzle ORM (better-sqlite3)
AI/ML Python sidecar (rembg, RealESRGAN, PaddleOCR, MediaPipe, LaMa)
Docs VitePress
Testing Vitest (unit/integration), Playwright (e2e)
CI/CD GitHub Actions, semantic-release, Docker multi-arch
Linting Biome (format + lint in one pass)

Commands

pnpm dev                  # Start all dev servers (web on :1349, api on :13490)
pnpm build                # Build all workspaces (turbo, packages first)
pnpm typecheck            # TypeScript check across monorepo
pnpm lint                 # Biome lint + format check
pnpm lint:fix             # Biome auto-fix
pnpm test                 # All Vitest tests (unit + integration)
pnpm test:unit            # Unit tests only
pnpm test:integration     # Integration tests only (full API)
pnpm test:e2e             # Playwright e2e tests (main app)
pnpm test:e2e:landing     # Playwright e2e tests (landing site)
pnpm test:e2e:docs        # Playwright e2e tests (docs site)
pnpm test:coverage        # Tests with V8 coverage report

# Run a single test file
pnpm vitest run tests/unit/my-test.test.ts
pnpm vitest run tests/integration/my-test.test.ts

# Run a single e2e spec
pnpm playwright test tests/e2e/my-test.spec.ts

# Database migrations (run from apps/api/)
cd apps/api && npx drizzle-kit generate   # Generate from schema changes
cd apps/api && npx drizzle-kit migrate    # Apply pending migrations

Key Conventions

  • Simplicity over complexity -- do not over-engineer
  • Double quotes, semicolons, 2-space indent (enforced by Biome)
  • ES modules in all workspaces ("type": "module")
  • Conventional commits for semantic-release (feat:, fix:, refactor:, docs:, test:, chore:)
  • Zod for all API input validation
  • i18n strings in packages/shared/src/i18n/en.ts (reference locale, ~1500 keys)
  • All user-facing strings use useTranslation() hook from apps/web/src/contexts/i18n-context.tsx
  • Use t.section.key for string references, format(t.key, { var }) for interpolation, plural(n, one, other) for pluralization
  • Tailwind uses logical properties for RTL support (ms- not ml-, text-start not text-left)
  • 21 supported languages. Adding a new locale: create packages/shared/src/i18n/<code>.ts typed as TranslationKeys, add to SUPPORTED_LOCALES in index.ts

Architecture: How a Tool Works

A "tool" is defined in three places that share the same toolId string:

  1. Shared metadata (packages/shared/src/constants.ts) -- TOOLS[] array with id, name, description, category, icon, route. Single source of truth for tool metadata consumed by both frontend and backend.

  2. API route (apps/api/src/routes/tools/<toolId>.ts) -- Processing logic. Most tools use the createToolRoute factory (apps/api/src/routes/tool-factory.ts), which handles multipart parsing, file validation, format decoding (HEIC/RAW/PSD), SVG sanitization, EXIF auto-orientation, Zod settings validation, output saving, preview generation, file versioning, and analytics. A simple tool is ~30 lines: a Zod schema + a process function that calls @snapotter/image-engine. Routes are registered in apps/api/src/routes/tools/index.ts.

  3. Frontend settings (apps/web/src/components/tools/<toolId>-settings.tsx) -- UI component for the tool's settings panel. Registered in apps/web/src/lib/tool-registry.tsx with a displayMode (side-by-side, before-after, live-preview, interactive-crop, interactive-eraser, no-dropzone, custom-results).

Adding a New Tool

  1. Add tool definition to TOOLS[] in packages/shared/src/constants.ts
  2. Create apps/api/src/routes/tools/<toolId>.ts -- export a register<Tool>(app) function that calls createToolRoute(app, { toolId, settingsSchema, process })
  3. Add to the registration array in apps/api/src/routes/tools/index.ts
  4. Create apps/web/src/components/tools/<toolId>-settings.tsx
  5. Add lazy import + registry entry in apps/web/src/lib/tool-registry.tsx
  6. Add i18n strings in packages/shared/src/i18n/en.ts

Request Lifecycle

Standard tools: Frontend sends POST /api/v1/tools/:toolId (multipart with file + JSON settings) via XHR with upload progress. The createToolRoute factory processes and returns {jobId, downloadUrl, originalSize, processedSize}.

AI tools: Do NOT use createToolRoute for their HTTP route. They return 202 Accepted immediately, process asynchronously via the Python sidecar, and stream progress via SSE (EventSource on /api/v1/jobs/:jobId/progress). They still register a sync wrapper via registerToolProcessFn() for pipeline/batch reuse.

Pipeline System

Pipelines chain tools: each step's output buffer feeds the next step's input. The tool registry (toolRegistry Map) enables pipelines and batch processing to call any registered tool's process function without duplicating logic. CRUD stored in pipelines DB table as JSON. Batch processing uses p-queue concurrency and returns a ZIP via archiver.

AI Sidecar (packages/ai/)

Two-tier Python execution:

  1. Persistent dispatcher (primary): Long-lived Python process (packages/ai/python/dispatcher.py) communicating via JSON over stdio. Node sends {id, script, args} on stdin, Python responds {id, stdout, exitCode} on stdout. Progress events stream on stderr as {progress, stage}. Pre-imports heavy ML libraries at startup to eliminate cold-start latency. Auto-restarts after 50 requests to prevent memory leaks.

  2. Per-request fallback: Fresh Python process per request if dispatcher is unavailable. Exponential backoff with crash recovery (5 crashes in 60s = permanent disable).

AI tools require model bundles defined in packages/shared/src/features.ts (FEATURE_BUNDLES). The API checks isToolInstalled() before processing; the frontend shows an install prompt if not installed.

Image Engine (packages/image-engine/)

processImage(input, operations[], outputFormat?) pipes a buffer through named operations. Each operation is a pure function (Sharp, options) => Promise<Sharp>. The OPERATION_MAP in engine.ts dispatches by string key. Individual operations are also exported for direct use by API routes.

Auth and Permissions

Session-based auth with scrypt password hashing. Three built-in roles (admin, editor, user) with 14 granular permissions defined in packages/shared/src/permissions.ts. Custom roles stored in roles table. API keys (prefixed si_) carry optional scoped permissions that intersect with user role permissions. Auth can be disabled entirely (synthetic anonymous user with user role).

Frontend State

14 Zustand stores in apps/web/src/stores/. The central one is file-store.ts managing FileEntry[] with blob URLs, processing status, and batch ZIP. Key hooks: use-tool-processor.ts (single-file XHR + batch + SSE progress) and use-pipeline-processor.ts.

All page components are lazy()-loaded. The key route is /:toolId rendering ToolPage (apps/web/src/pages/tool-page.tsx), which looks up metadata from shared constants and the frontend tool registry.

Database

SQLite via Drizzle ORM. Schema: apps/api/src/db/schema.ts. Migrations in apps/api/drizzle/.

Tables: users, teams, sessions, settings (key-value), jobs, apiKeys, pipelines, auditLog, roles, userFiles.

Testing

  • tests/unit/ -- Vitest unit tests
  • tests/integration/ -- Vitest integration tests (one per tool/feature, full API)
  • tests/e2e/ -- Playwright specs (Chromium primary, Firefox/WebKit for cross-browser)
  • tests/fixtures/ -- Small test images in various formats

Vitest uses single-fork pool (shared SQLite connection), 30s timeouts, and injects test env vars. Playwright auth setup project logs in and saves storage state; spins up both dev servers with a fresh test DB.

Pre-commit Hooks

Husky + lint-staged runs biome check --write on staged *.{ts,tsx,js,jsx,json} files before every commit. If a commit fails, fix the lint issues rather than bypassing the hook.

Do Not Modify Config Files

Biome, TypeScript, and editor config files are protected by hooks. Fix the code to satisfy the linter/compiler, not the other way around. This prevents a common AI failure mode where rules get weakened instead of code getting fixed.

Model Routing for Subagents

Always use Opus 4.7, Max Effort, 1M context for every agent and subagent. No exceptions. No model downgrading.

Strategic Compaction

When context gets large, compact at logical phase boundaries:

  • Good times to compact: After research and before planning. After debugging and before implementing the fix. After completing a major feature.
  • Bad times to compact: Mid-implementation. While actively debugging. During a multi-step refactor.
  • Survives compaction: This CLAUDE.md, active tasks, git state, memory files
  • Lost on compaction: Intermediate reasoning, file contents previously read, conversation flow