CLAUDE.md
August 2, 2026 · View on GitHub
Guidance for Claude Code in this repo — conventions, rules, and gotchas only. Read the code and docs/ for anything derivable from them.
CRITICAL: published as the npm package
@libredb/studioand consumed bylibredb-platform. UI/styling work MUST follow the platform-integration rules in.claude/rules/platform-integration.md— auto-loaded when you touch components /.tsx/globals.css. Violations cause silent style/layout breakage that appears only when embedded in platform.
Project Overview
Web-based SQL IDE for cloud-native teams: PostgreSQL, MySQL, SQLite, Oracle, SQL Server, MongoDB, Redis + AI query assistance. Runs two ways — a standalone Next.js app AND an embedded npm package inside libredb-platform; build:lib (tsup) produces the package dist. Verify any UI change in both modes.
Branching & PRs
Trunk-based: feature/work branches target
maindirectly; releases are git tags. Branch offmainfor new work and open every PR with basemain(gh pr create --base main).mainis the single protected integration trunk — PRs are required and theLint, Typecheck and BuildandUnit & Integration Testschecks must pass before merge (SonarCloud still runs on push and same-repo PRs but is not a required check: fork PRs cannot produce it, which used to hard-block them). Cut a release by taggingmain— tags carry novprefix (0.9.65, notv0.9.65) — and always follow the/cut-releaseskill (.claude/skills/cut-release/SKILL.md), which holds the full runbook: bump, pre-tag verification, draft-first publish, the ref-pinned dispatch chain and the recovery table. It is user-invoked only, so ask for it rather than improvising a release. There is nodevbranch and no long-livedrelease/*branches. A PR that bumps thepackage.jsonversion must also runbun run chart:bumpandmake -C operator bundleand commit both results — the required CI check enforcesChart.yaml appVersion==package.jsonversion (#138), and a second gate fails whenoperator/bundle/operator/configstill carry the previous version (the OLM CSV takes its version and controller image tag frompackage.json). Tag only after both are committed: the tag ref is what the operator image and bundle are built from, so a bundle refreshed later lives onmainonly. A PR that changes any packaged file undercharts/libredb-studio/must ALSO bumpChart.yaml versionwhenever the current chart version is already released — the same required check enforces it (#167), andchart:bumpwill not do it for you whileappVersionis already in sync, so bumpversion:and the README--versionexamples by hand.
GitHub
- Repo: https://github.com/libredb/libredb-studio
- Image (canonical):
ghcr.io/libredb/libredb-studio:latest— use GHCR in all copy-paste examples (Docker Hublibredb/libredb-studiois a discoverability mirror only) - Helm: repo
https://libredb.org/libredb-studio/· OCIoci://ghcr.io/libredb/charts/libredb-studio· ArtifactHub
Development Commands
bun install # deps (Bun preferred)
bun dev # dev server (Turbopack)
bun run build # production build
bun run format # Biome formatter check (format:fix to write); CSS/JSON excluded
bun run lint # oxlint (fast, syntactic) then ESLint 9 (eslint-config-next + narrow type-aware layer)
bun run lint:oxc # oxlint only
bun run typecheck # TypeScript strict
bun run test # all layers: unit + api + integration + hooks + components
bun run test:e2e # Playwright (requires build)
bun run test:coverage # coverage report (merged lcov)
bun run coverage:check # enforce 100% line coverage on the merged lcov (CI gate)
bun run build:lib # tsup → @libredb/studio package dist (see rule below)
bun run attw # validate published type-resolution against the packed tarball (needs build:lib first)
Toolchain rationale (Biome formatter, oxlint, type-aware ESLint layer, attw) lives in
docs/TOOLCHAIN.md. Biome is formatter-only (lineWidth 120); oxlint is the fast syntactic layer in front of ESLint;eslint-config-nextstill owns React/Next/hooks; a narrowtypescript-eslinttype-aware layer guardssrc/app/api+src/lib/dbagainst floating promises; attw uses--profile node16(the package targets Node >=24 + modern bundlers, so node10 is ignored).
build:libafter platform-facing changes: after changing any component used by platform (workspace, providers, …), runbuild:lib—bun run build(Next.js) does NOT update the package dist.
Tests — always
bun run test, never barebun test. Component tests need isolated execution groups (tests/run-components.sh) to avoidmock.module()cross-contamination.
Coverage isolation:
bun'smock.module()is process-wide — a file mocking a shared module (@/lib/db/factory,@/lib/oidc, …) poisons others sharing the process → nondeterministic CI failures (clearProviderCache is not a function,Export named 'removeProvider' not found). Sotest:coverage:coreruns each core test file in its ownbunprocess viatests/run-core.sh;test:coveragemerges per-file lcov. Do NOT collapse this into a singlebun test tests/unit tests/api tests/integrationinvocation.
Pre-Commit Verification (MANDATORY)
After every code change, run all six locally before claiming done — they match CI (ci.yml, docker-build-push.yml): bun run format · bun run lint · bun run typecheck · bun run knip · bun run test · bun run build. A local pass guarantees CI passes; do not skip any. (bun run lint runs oxlint then ESLint; knip fails on unused files/exports/dependencies; the CI lint-and-build job additionally runs build:lib + attw.)
100% line coverage is a hard CI gate — work TDD, always. The merged lcov must stay at 100% (
scripts/check-coverage.mjsfails theUnit & Integration Testsjob otherwise), so every change that adds or alters executable lines MUST land with tests covering them in the same PR. The sustainable way to satisfy this is test-driven development as the default working style — write the failing test first, then the implementation — even when nobody asks for it; retrofitting tests after the code is how uncovered branches and coverage-gate fights accumulate. Verify locally withbun run test:coverage && bun run coverage:check— it prints the exact uncovered file:line ranges. Coverage-measurement rationale (per-function lcov granularity, phantom lines, the authority-universe merge rule,run_group --nocov) lives indocs/TOOLCHAIN.md.
Architecture
- Stack: Next.js 16 (App Router) + React 19 + TypeScript; Tailwind 4 + Shadcn/UI; Monaco editor; TanStack Table + react-virtual;
joseJWT +openid-clientOIDC. - DB drivers:
pg,mysql2,bun:sqlite/node:sqlite(the DB provider, runtime-selected;LIBREDB_SQLITE_DRIVERoverrides) /better-sqlite3(the storage layer),oracledb,mssql,mongodb,ioredis. - Layout: full tree + data flow in
docs/ARCHITECTURE.md. Key dirs:src/lib/db(DB providers, Strategy Pattern),src/lib/llm(LLM providers),src/lib/storage(pluggable persistence),src/workspace+src/exports(the npm-package embedding layer),src/proxy.ts(RBAC middleware). - Path alias:
@/*→./src/*.
Rules & patterns
⚠️ Providers are the lifeblood of this project — keep the triad in lockstep: code ↔ docs ↔ tests, 1:1 per canonical type-id (
postgres,mysql,sqlite,oracle,mssql,mongodb,redis, plus the embeddedlibredb):
- Code:
src/lib/db/providers/<family>/<type-id>.ts· Docs:docs/providers/<type-id>.md· Tests:tests/integration/db/<type-id>-provider.test.ts- Any change to one side MUST sync the others in the same PR. The doc mirrors the code and the code mirrors the doc — never let them drift.
- DB abstraction: Strategy Pattern. SQL providers extend
SQLBaseProvider; MongoDB/Redis extendBaseDatabaseProvider. No=== 'mongodb'type-checks outside provider classes — drive behaviour through capabilities/labels. - Auth:
NEXT_PUBLIC_AUTH_PROVIDER=local(email/password) oroidc(PKCE → same JWT cookie as local).src/proxy.tsenforces RBAC (admin vs user). Details:docs/OIDC.md. - Storage: write-through cache — localStorage serves reads;
useStorageSyncpushes mutations to the server (debounced).STORAGE_PROVIDER(server-side only) =local|sqlite|postgres. Details:docs/STORAGE.md. - API routes: all backend in
src/app/api/; JWT-protected except/login,/api/auth,/api/db/health.
Configuration
Env vars are documented with examples in .env.example. Non-obvious ones: NEXT_PUBLIC_AUTH_PROVIDER (local | oidc); OIDC_* required when oidc; STORAGE_PROVIDER / STORAGE_SQLITE_PATH / STORAGE_POSTGRES_URL are server-side only (not NEXT_PUBLIC_), discovered at runtime via /api/storage/config.
Database Connections
Connections are typed by type; per-provider fields, query formats, and behaviours are in docs/providers/<type-id>.md and docs/API_DOCS.md.
Redis maps onto the SQL-oriented provider interface by convention: getSchema() uses a non-blocking SCAN (never KEYS *), grouping key prefixes as "tables"; health/metrics from INFO; slow queries / sessions from SLOWLOG GET / CLIENT LIST. See docs/providers/redis.md.
Docker & Helm
- Docker: multi-stage Bun build, standalone Next.js output. Build args
JWT_SECRET_BUILD,ADMIN_PASSWORD_BUILD,USER_PASSWORD_BUILD. Health checkGET /api/db/health. - Helm: lint with
helm lint charts/libredb-studio --strict. Full values reference:charts/libredb-studio/README.md; chart architecture/rationale:docs/HELM_CHART.md.