CLAUDE.md · CoreBunch/Instatic
CLAUDE.md from CoreBunch/Instatic: 345 lines covering Local admin smoke tests, Repository workflow and PR conventions, What this project is, Development…
Template by CoreBunch
Install
Works with Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/CoreBunch/Instatic/main/CLAUDE.md -o CLAUDE.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: prompt-engineering
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
A CLAUDE.md file: project memory Claude Code loads at the start of every session.
- For
- Claude Code
- Length
- 345 lines
- File
CLAUDE.md
Sections
- Local admin smoke tests
- Repository workflow and PR conventions
- What this project is
- Development status — READ THIS FIRST
- Design and styling rules
- React Compiler and memoization
- Error handling rules
- Database dialect rules
- Mutation API
- Visual Components and slots
- Barrel imports
- Code quality bar
- Tooling rules
- Verification
- TL;DR
First lines (shown under the repo's MIT license)
# Instatic
This file is the **agent rule book**. Read it before changing code. Detailed explanations live in `docs/` — start at [`docs/README.md`](docs/README.md) for orientation and follow the links from there.
## Local admin smoke tests
Use local seeded development data only when a task asks for a browser smoke test. Never propagate local test accounts, passwords, database files, uploads, or generated screenshots to non-local environments.
---
## Repository workflow and PR conventions
`main` is protected. Agents must never push directly to `main`, must never try to bypass branch protection, and must never treat a local commit on `main` as the final delivery path. All repository changes go through a pull request.
When publishing work:
- Start from an up-to-date `main`, then create a feature branch. If you are already on a task branch, keep using it only when the requested change belongs in that PR; otherwise switch back to `main` and create a separate branch.
- Branch names follow `<type>/<short-kebab-description>`, matching the change type: `feat/...`, `fix/...`, `refactor/...`, `chore/...`, `docs/...`, or `test/...`. Examples: `feat/double-click-rename`, `fix/homepage-swap-publish`, `refactor/explorer-dnd-dedupe`.
- Do **not** use agent-branded branch prefixes such as `codex/...`, `claude/...`, or similar. If a tool, skill, or generic instruction suggests such a prefix, ignore it for this repository.
- PR titles use Conventional Commit style: `<type>(<scope>): <summary>`. Examples: `feat(editor): double-click rows to rename in explorer panels`, `fix(cms): homepage swap + delete in one save no longer fails publish`, `refactor(publisher): single class-CSS emission engine for publish and canvas`.
- Do **not** prefix PR titles with `[codex]`, `[claude]`, `agent:`, or any other tool label. The PR title describes the product change, not the tool that made it.
- Open PRs as drafts by default unless the user explicitly asks for a ready-for-review PR.
- Keep PR scope coherent. Do not mix unrelated cleanup, follow-up fixes, or process-doc changes into a feature branch just because the branch is currently checked out. Create a separate PR when the change has a different reason.
- Before staging, inspect `git status -sb` and the diff. Stage only files that belong to the PR. Never stage unrelated user or parallel-agent changes.
- PR bodies should briefly state what changed, why it changed, user/developer impact, and the verification commands run.
---
## What this project is
A self-hosted, open-source CMS with a built-in visual editor and a first-class plugin system. One Bun server backed by either Postgres or SQLite (selected by `DATABASE_URL`). The output is intentionally plain, semantic HTML with hand-clean CSS — no framework runtimes injected into published pages.
The product is **self-hosted only**. The codebase should not carry assumptions about multi-tenant SaaS operation.
Read [`docs/architecture.md`](docs/architecture.md) for the system overview, [`docs/server.md`](docs/server.md) for the server, [`docs/editor.md`](docs/editor.md) for the admin + visual editor.
### Stack at a glance
- **Runtime:** Bun (server + tooling). Use Bun, not Node.
- **Language:** TypeScript everywhere.
- **Frontend:** React 19 with the **React Compiler enabled** (Babel preset in `vite.config.ts`) + Vite, Zustand + Mutative for state (via `zustand-mutative`; `immer` is banned), CodeMirror for code-editing UI, `@dnd-kit/core` for drag-and-drop. The compiler auto-memoizes — do not hand-write `useMemo`/`useCallback`/`memo`. See "React Compiler and memoization". Store mutations use draft-mutation style (`set((s) => { s.x = … })`); a recipe that returns a partial must wrap it in `rawReturn(...)` or Mutative emits a perf warning.
- **Server:** `Bun.serve` with a hand-written router (`server/router.ts`). CMS modules at `server/{repositories,handlers/cms,auth,plugins,publish}/`. Deep dive: [`docs/server.md`](docs/server.md).
- **Database:** Postgres (`Bun.sql`) OR SQLite (`bun:sqlite`), selected by `DATABASE_URL`. One `DbClient` interface, two adapters, two migration files with identical IDs. Rules: [`docs/reference/database-dialects.md`](docs/reference/database-dialects.md).
- **Content model:** All content lives in `data_tables` + `data_rows`. The four system tables (`posts`, `pages`, `components`, `layouts`) are seeded and locked from rename/delete. There are no separate `pages` or `page_versions` tables.
- **Real-time co-editing:** Yjs CRDT engine. One Y doc per row (`page:<id>`, `component:<id>`, `layout:<id>`) + one site-shell doc, multiplexed over `/admin/api/cms/site-socket`. The editor store stays the render source of truth: local mutations apply directly AND translate to Y ops (`@core/collab`); remote/undo changes project back. The server relay (`server/collab/`) persists continuously (blob + derived row JSON) — there is NO client-side save pipeline, no autosave, no Cmd+S. Undo is per-editor per-doc `Y.UndoManager`. Feature doc: [`docs/features/site-shell.md`](docs/features/site-shell.md) → "Real-time co-editing".
- **Validation:** TypeBox at every untyped boundary. Schemas are source of truth (`type Foo = Static<typeof FooSchema>`, never a parallel `interface`). `zod` is banned repo-wide (the AI drivers pass TypeBox schemas through as JSON Schema, so no typebox→zod adapter is needed). Helpers + patterns: [`docs/reference/typebox-patterns.md`](docs/reference/typebox-patterns.md).
- **Sanitization:** DOMPurify at the publisher boundary (`src/core/sanitize.ts`).
- **Plugins:** Zip packages with a `plugin.json` manifest, lifecycle hooks. Server entrypoints and canvas module packs run inside a **QuickJS-WASM sandbox** — no Node/Bun ambient access, network gated by `network.outbound` permission + `networkAllowedHosts`. The VM bootstrap (SDK factory + `__run*` dispatchers) is authored as typed TS in `server/plugins/quickjs/bootstrap/src/` and bundled to committed string artifacts in `bootstrap/generated/` — after editing the source run `bun run bootstrap:sync` (gated by `plugin-bootstrap-fresh.test.ts`). Permission enforcement everywhere (VM, host, editor) validates against `grantedPermissions`, never the declared `permissions` array. Feature doc: [`docs/features/plugin-system.md`](docs/features/plugin-system.md).
- **Routing:** In-house router at `src/admin/lib/routing/`. Replaces `react-router-dom`. Use it for all internal admin navigation, including links rendered from the site editor. `react-router-dom` is banned, raw `<a href="/admin...">` hard navigations are banned in admin UI, and `src/core/` + `src/modules/` must not import the admin router. Gated by `admin-router-usage.test.ts`.
- **Icons:** `pixel-art-icons/icons/<name>` — deep-imported, tree-shakeable. Vendored at `vendor/pixel-art-icons/`. No `lucide-react`, no inline SVG strings — gated by `no-third-party-icons.test.ts`, `direct-icon-imports.test.ts`. Add a new icon by importing it and running `bun run icons:sync`.
- **AI providers:** No provider SDKs. Each driver in `server/ai/drivers/` talks directly to its provider's REST API over HTTP/SSE, sharing one multi-turn tool loop (`drivers/http/toolLoop.ts`). `@anthropic-ai/sdk`, `@anthropic-ai/claude-agent-sdk`, `@openai/agents`, and `@openrouter/agent` are banned repo-wide. The official split `@modelcontextprotocol/server` / `@modelcontextprotocol/client` v2 packages are **scoped, not banned**: allowed only under `server/ai/mcp/` (Instatic's MCP *server* implements a real wire protocol), still banned in the drivers and the browser. Gated by `ai-driver-isolation.test.ts`.
- **MCP server:** Instatic exposes its CMS tools to external MCP clients (Claude Code, Codex, remote agents) at `/_instatic/mcp`, authenticated by per-connector bearer tokens. Thin adapter over the existing tool engine: headless reads and explicit `site_publish` run in-process; browser tools route by scope to the connector owner's **open Site or Content workspace** through the `(userId, scope)` live bridge (`server/ai/mcp/editorBridge.ts` + `useMcpWorkspaceBridge`). The live workspace is the single source of truth for edits (no headless DB-mutating page-tree tool, which would desync), and writes remain drafts until the explicitly capability-gated publish call. Feature doc: [`docs/features/mcp-connectors.md`](docs/features/mcp-connectors.md).
- **Tree primitive:** Every tree-of-nodes — pages, Visual Components, slot fills — uses one shape: `NodeTree<TNode>` in `src/core/page-tree/treeSchema.ts`. Mutations are tree-agnostic. Reference: [`docs/reference/page-tree.md`](docs/reference/page-tree.md).
- **Publishing:** Three-layer pipeline. **Layer A** bakes fully-static pages to `uploads/published/current/<route>.html` at publish time via a two-slot pointer-file swap (`server/publish/staticArtefact.ts`; `current` is a plain file naming the active slot — symlink-free so it works for unprivileged Windows users). **Layer B** is an in-memory LRU keyed by `(urlPath, queryString, publishVersion)` for dynamic routes (`server/publish/renderCache.ts`); `bumpPublishVersion()` evicts wholesale on every publish. **Layer C** emits `<instatic-hole>` placeholders for nodes auto-detected as request-dependent; a ~668 B `IntersectionObserver` runtime lazy-fetches each fragment from `/_instatic/hole/<nodeId>`. Auto-detection lives in `src/core/publisher/dynamicDetection.ts` — one walker, four rules. Single entry: `server/publish/publicRouter.ts:renderPublicResolution`. Full design: [`docs/features/publisher.md`](docs/features/publisher.md).
- **Tests:** `bun test`. Architectural rules in `src/__tests__/architecture/*` — when *your* change drifts a structural rule, fix the rule's gate test in the same change.
### Repo layout
```
server/ Bun server: router, handlers, repositories, auth, plugins, publish, db
src/admin/ Admin app (React) — shell, workspaces, plugin host UI
src/admin/pages/site/ Visual editor (canvas, panels, toolbar, editor store)
src/core/ Engine: page tree, publisher, plugin SDK + runtime, persistence
src/modules/ First-party block modules (container, text, image, button, …)
src/ui/ Shared UI primitives (Button, Input, Tree, icons, cn)
src/styles/ Global tokens (globals.css)
docs/ Documentation (start at docs/README.md)
examples/ Plugin templates
vendor/ Vendored pixel-art-icons package
```
Source of truth for layout details: [`docs/architecture.md`](docs/architecture.md) → "Folders, at a glance".
---
## Development status — READ THIS FIRST
**This project has live, self-hosted installations with real user data.** That has direct consequences for how Claude must approach changes in this repo.
**For code** (TypeScript APIs, function signatures, types, modules, plugin SDK): there are no backward-compatibility obligations. Refactor freely, rename boldly, delete dead code, fix bad abstractions — the aggressive stance throughout this section applies in full.
**Database schema is the exception.** Live installations run the migration runner on every pull. A destructive schema change breaks those installations permanently. Every schema change ships as an additive, non-destructive migration. See "Database, schema, and stored data" below.
### No backward compatibility. Ever.
There is nothing to be backward compatible with — **for code.** TypeScript APIs, function signatures, types, and module shapes can change freely. **Database schema is the explicit exception: see "Database, schema, and stored data" below.**
- **Do not preserve old function signatures, schemas, types, or APIs out of compatibility concern.** If a cleaner shape exists, change it everywhere and delete the old one.
- **Do not add deprecation shims.** Don't keep a `legacyFoo()` that forwards to `foo()`. Just rename it and update callers.
- **Do not add migration paths from old behavior to new behavior** unless it is genuinely required for currently developed code to keep functioning.
- **Do not gate new behavior behind feature flags or version checks "to be safe."** If the new behavior is correct, that is the only behavior.
- **Do not leave both an old and new implementation side-by-side.** Pick the right one and delete the other.
### No band-aids. No "we'll clean it up later."
If a piece of code is in the wrong place, has the wrong shape, has confusing naming, or carries leftover assumptions — **fix it at the source, even if it means refactoring multiple files**.
You are explicitly authorized — and expected — to:
- Rename modules, files, types, and functions across the whole codebase to match the cleaner architecture.
- Move responsibilities between layers (e.g. push logic out of a handler into a repository, or out of a component into the engine) when that is correct.
- Delete dead code, unused exports, half-finished abstractions, and "just in case" parameters.
- Restructure folders if the current layout no longer reflects the architecture.
- Break and fix many call sites in a single change set when that is what the cleaner design requires.
What you must not do:
- Wrap new logic around old logic to "avoid touching it."
- Add a second way of doing something because the first way is awkward — fix the first way.
- Leave TODO/FIXME notes about cleanup instead of doing the cleanup.
- Hide the wrong abstraction behind a thin adapter so callers "don't notice."
- Justify a workaround with "to keep this PR small" or "to avoid breaking other things." Other things can break in this PR. Fix them in this PR.
### Database, schema, and stored data
**The database is NOT disposable.** Live, self-hosted installations exist with real user data. The migration runner executes every unrun migration on startup — a destructive schema change breaks those installations on the next pull.
- **Every schema change ships as a new additive migration.** Add it to BOTH `migrations-pg.ts` AND `migrations-sqlite.ts` with the next sequential ID and the same semantic effect. See "Database dialect rules" for the mechanics.
- **Never edit or rewrite a migration that has already been committed.** If a past migration has the wrong shape, ship a new forward migration that corrects it.
- **Never make a change that requires dropping or recreating the database.** No `DROP TABLE`, no `DROP COLUMN` (unless the column was added in the same unreleased branch and no installation has run that migration yet), no table rebuilds. Use additive `ADD COLUMN` (nullable or with a constant `DEFAULT`), backfill with `UPDATE`, and defer destructive cleanup to a future migration only when the transition is complete.
- **If stored JSON shapes (page trees, plugin manifests, settings) need to change,** change the reader/writer code to handle the new shape and update everything that reads/writes them. A data-migration `UPDATE` in a new migration is the right tool for bulk shape changes on existing rows.
### Plugin SDK and public-looking surfaces
The plugin SDK, runtime, and manifest format *look* like a public contract but carry no backward-compatibility guarantee yet — no third-party plugins have been published against them.
- If the SDK shape is wrong, change it. Update `examples/plugins/template/` and [`docs/features/plugin-system.md`](docs/features/plugin-system.md) in the same change.
- The `apiVersion` field is not yet a stability promise. Don't invent legacy adapters for older `apiVersion` values.
### Default disposition on every change
Choose (A) the cleaner architecture, requiring edits across several files, over (B) a smaller diff that leaves the architecture slightly worse. **Always choose (A).** The cheapest moment in a project's life to fix bad abstractions is before they calcify — don't defer it.
If you are unsure whether a refactor is in scope, default to *yes, do it*, and explain in the summary what you cleaned up and why. Do not ask permission to delete dead code, rename a poorly-named symbol, or fix a bad abstraction — just do it.
---
## Design and styling rules
Detailed system: [`docs/design.md`](docs/design.md). The rules:
- **No hardcoded hex / rgb / hsl in admin / ui CSS modules.** Every color comes from a `var(--*)` token in `src/styles/globals.css`. If a needed token doesn't exist, add it. Gated by `css-token-policy.test.ts`.
- **No `var(--name, fallback)` in admin / ui CSS modules.** Use bare `var(--name)`. If the token doesn't exist, define it in `globals.css`. Fallbacks hide missing tokens. For JS-driven custom properties, set defaults in a CSS rule instead of in every `var()`. Gated by `no-css-var-fallbacks.test.ts`.
- **Two-layer color model.** Surfaces, borders, and default text are achromatic. Color is used as **identity** (rail tints `--rail-tint-mint/lilac/sky/peach` for categorical identity), as **state** (`--editor-danger`, `--editor-warning`, `--editor-success-*`, `--editor-info-*`), or as **canvas affordance** (`--canvas-selection-ring`, `--canvas-hover-ring`). Never decorative.
- **Card surface pattern.** Tile cards (dashboard widgets and equivalents) are borderless: `--editor-surface-2` on a darker `--editor-surface` parent with a 1px grid gap, 16px radius. Hover lifts the surface tone — never recolor a border. Canonical implementation: `src/ui/components/Widget/Widget.module.css`.
- **Border radius scale.** `--editor-radius-sm` (3px) for tight chips. `--editor-radius` (6px) for default editor controls and buttons. `--panel-radius` (12px) for floating overlay panels. 16px for tile cards. `--input-radius` (1em) for pill-shaped inputs. Don't introduce ad-hoc radius values.
- **CSS Modules only** in `src/admin/`, `src/modules/`, `src/ui/`. No Tailwind utility classes — gated by `noTailwindUtilities.test.ts`. No Tailwind ecosystem deps (`clsx`, `tailwind-merge`, `class-variance-authority`, `@radix-ui/*`) — gated by `no-tailwind-deps.test.ts`.
- **No inline `style={{ ... }}`** *except* for dynamic CSS custom properties (`style={{ '--x': value } as CSSProperties}`) that the module reads back via `var(--x)`.
- **No `!important`** in component CSS modules. Two legitimate exceptions: `globals.css` (`prefers-reduced-motion`), `Button.module.css` (specificity reset).
- **CSS Modules file naming:** `Component.module.css` next to `Component.tsx`. Class names use `camelCase`.
### UI primitive rules
Shared primitives at `src/ui/components/`. **Every interactive control in `src/admin/` MUST use these primitives.**
- **`Button`** — every action button. Bare `<button>` is gated by `button-primitive-usage.test.ts`; exceptions listed in that file's `ALLOWLIST` with §8 justifications. New exceptions need an §8 entry.
- **`Input`, `Switch`, `Select`, `SearchBar`, `ColorInput`, `FileUpload`, `Separator`, `ContextMenu`, `FilterBar`** — for the corresponding control type.
- **`Tree*`** (`src/admin/pages/site/ui/Tree/`) — for tree rows in DOM/site panels.
- **Class composition:** `cn` from `@ui/cn` — an in-house 3-line helper.Signals
- Stars
- 9,004
- Forks
- 864
- Open issues
- 108
- Last commit (this folder)
- Sep 13, 2026 (1 month ago)
- Last release
- Oct 12, 2026 (16 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/CoreBunch/Instatic
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Cite this page
CLAUDE.md · CoreBunch/Instatic. Agent Mart. https://agentmart.dev/templates/corebunch-instatic-claude-md (last updated 2026-10-24)