Guidance for Claude Code in this repo — conventions, rules, and gotchas only. Read the code and docs/ for anything derivable from them.
This repo is published as the npm package
@libredb/studio— a CLI (npx @libredb/studio) plus an embeddable library surface built bybuild:lib. It is not embedded inlibredb-platform: separate products since 2026-08-14, so "platform consumes this" is never a reason to keep or avoid anything (.claude/rules/platform-integration.mdwas deleted with that decision).
Web-based SQL IDE for cloud-native teams: eighteen external engines, plus the embedded LibreDB store (EXTERNAL_DATABASE_TYPES in src/lib/db/compatibility.ts is the external-engine list, never a prose enumeration), plus AI query assistance. It runs two ways, as a standalone Next.js app and as a published npm package, and the two render different chrome, so a UI change verified in one is not verified in the other.
Trunk-based: feature branch →
main→ tag. Open every PR withgh pr create --base main; there is nodevbranch and no long-livedrelease/*branches.mainis protected: PRs required, andLint, Typecheck and Build,Unit & Integration TestsandSecret Scanmust pass (SonarCloud runs but is not required — fork PRs cannot produce it).Tag namespace. Product releases are bare semver tags on
main— novprefix; thev-prefixed tags below 0.9.28 are frozen history, and chart releases use a separatelibredb-studio-<chart version>namespace. Sogit tag | tailis not "the latest release". Cutting one is the user-invoked/cut-releaseskill (.claude/skills/cut-release/SKILL.md) — ask for it, never improvise.Two version gates, both enforced by the required check. A PR bumping the
package.jsonversion must also runbun run chart:bumpandmake -C operator bundleand commit both (#138; the OLM CSV takes its version and controller image tag frompackage.json) — tag only once both are onmain, since the tag ref is what the operator image and bundle build from. A PR changing any packaged file undercharts/libredb-studio/must ALSO bumpChart.yaml versionby hand when the current chart version is already released (#167) —chart:bumpskips it whileappVersionis in sync — plus the README--versionexamples.
- Repo: https://github.com/libredb/libredb-studio
- Image (canonical):
ghcr.io/libredb/libredb-studio:latest— use GHCR in every copy-paste example (Docker Hublibredb/libredb-studiois a mirror only) - Helm: repo
https://libredb.org/libredb-studio/· OCIoci://ghcr.io/libredb/charts/libredb-studio· ArtifactHub
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
bun run lint:oxc # oxlint only
bun run typecheck # TypeScript strict
bun run test # every test file under tests/ (except tests/live/), one bun process per file
bun run test:unit # one layer; also test:api, test:integration, test:hooks, test:security, test:evals, test:components
bun run test:e2e # Playwright (builds and starts its own servers; see playwright.config.ts)
bun run test:coverage # coverage report (merged lcov)
bun run coverage:check # enforce 100% line coverage on the merged lcov
bun run build:lib # tsup → @libredb/studio package dist (see rule below)
bun run attw # type-resolution check against the packed tarball (run build:lib first)
# drift guards — all four run inside the required "Lint, Typecheck and Build" check:
bun run chart:check # chart version sync guard (#138; CI sets CHART_SYNC_STRICT=1 and fetches origin/main)
bun run channels:showcase:check # login channel showcase drift guard (#425)
bun run readme:check # localized README drift guard (#317)
bun run security:check # security posture drift guardToolchain rationale (Biome formatter-only, oxlint in front of ESLint, the narrow type-aware layer, attw) lives in
docs/TOOLCHAIN.md. Read it before changing any lint, format or packaging config.
Run
build:libafter changing anything reachable fromsrc/exports/(workspace, providers, components, security, …) —bun run build(Next.js) does NOT update the package dist.
Tests, always
bun run test, never barebun testover a directory. The runner (tests/run-tests.ts) discovers every test file and runs each one in its own bun process, several at a time (--jobs=N,--list).bun test tests/apiputs all of them in one process instead, where one file'smock.module()becomes every file's, because bun's module mocks are process-wide with no undo. To run one file, name it:bun tests/run-tests.ts tests/unit/x.test.ts.
Coverage:
bun run test:coverageis the same runner with--coverage --merge-into=coverage/lcov.info: one lcov per test file, merged byscripts/merge-lcov.mjs. Two files run without coverage on purpose; they areCOVERAGE_EXEMPT_FILESintests/runner/discover.ts, with the reason in its docblock. Rationale:docs/TOOLCHAIN.md.
Run the required-check gate set locally before claiming done. .github/workflows/ci.yml is the authority — this list mirrors it and can fall behind it:
bun run format && bun run lint && bun run typecheck && bun run knip \
&& bun run chart:check && bun run channels:showcase:check \
&& bun run readme:check && bun run security:check \
&& bun run test && bun run buildA clean local pass is still not a guarantee: the same job also runs build:lib + attw and gofmt/go vet/go test over packaging/windows/launcher, and the coverage gate below lives in a separate required job.
100% line coverage is a hard CI gate — work TDD, always.
scripts/check-coverage.mjsfails the requiredUnit & Integration Testsjob below 100%, so every change that adds or alters executable lines lands with its tests in the same PR — write the failing test first, even unasked.bun run test:coverage && bun run coverage:checkprints the exact uncovered file:line ranges. Measurement rationale:docs/TOOLCHAIN.md.
- DB drivers: the two build configs do NOT externalize the same set, so read
next.config.tsandtsup.config.tsrather than any prose list.pg,mysql2,cassandra-driver,mongodbandoracledbare external in both;mssqlandioredisare external in the library build only and Turbopack bundles them into the server chunk. That is not always harmless:oracledbwas in exactly that position until #538, and bundling rewrote its__dirnameto/ROOT/..., so Thick mode could never load its native addon. Two traps: SQLite isbun:sqlite/node:sqlitefor the DB provider (runtime-selected,LIBREDB_SQLITE_DRIVERoverrides) butbetter-sqlite3for the storage layer;@duckdb/node-apiis a native N-API addon (~68 MB of bindings per libc variant), external too. - Layout: tree + data flow in
docs/ARCHITECTURE.md. Key dirs:src/lib/db(providers),src/lib/llm,src/lib/storage,src/workspace+src/exports(the npm-package library surface),src/proxy.ts(RBAC middleware).
⚠️ Providers are the lifeblood of this project — keep the triad in lockstep: code ↔ docs ↔ tests, 1:1 per canonical type-id — the type-id set is theDatabaseTypeunion insrc/lib/types.ts, which is the only list:
- Code:
src/lib/db/providers/<family>/<type-id>.ts, or.../<type-id>/index.tswhen the provider is split across modules · Docs:docs/providers/<type-id>.md· Tests:tests/integration/db/<type-id>-provider.test.ts- One directory may serve two type-ids —
sql/search/is bothelasticsearchandopensearch(#424). Docs and tests stay 1:1 anyway: the invariant is per type-id.- 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-dialect providers extend
SQLBaseProvider; the non-SQL ones (mongodb,redis,couchbase,prometheus,kafka,libredb) extendBaseDatabaseProviderdirectly, andSQLBaseProvideritself extends it. Insidesrc/lib/db, never branch on the type id; drive behaviour through capabilities/labels. Three=== "mongodb"branches survive in the UI layer as known debt (src/hooks/use-connection-form.ts,src/lib/editor/tab-language.ts,src/components/ConnectionModal.tsx); do not add a fourth. - Auth:
NEXT_PUBLIC_AUTH_PROVIDER=local(email/password) oroidc(PKCE → the same JWT cookie);src/proxy.tsenforces RBAC (admin vs user).docs/OIDC.md. - Storage: write-through cache — localStorage serves reads,
useStorageSyncpushes mutations to the server (debounced).STORAGE_PROVIDER(server-side only) =local|sqlite|postgres.docs/STORAGE.md. - API routes: all backend in
src/app/api/, except/health, which is a route at the app root so the plainest liveness path exists; JWT-protected except the public set insrc/proxy.ts—/login,/api/auth/*,/health,/api/health,/api/db/health,/api/storage/config,/_next,/favicon.icoand static assets — plus two machine paths gated by bearer credentials of their own instead of the JWT: the agent drive callback and /api/mcp.src/proxy.tsis the authority; do not restate the list elsewhere.
Every env var is documented with an example in .env.example. The one thing that file cannot show you: STORAGE_PROVIDER / STORAGE_SQLITE_PATH / STORAGE_POSTGRES_URL are server-side only (not NEXT_PUBLIC_) and are discovered at runtime via /api/storage/config.
Connections are typed by type; per-provider fields, query formats and measured behaviours live in docs/providers/<type-id>.md and docs/API_DOCS.md. The non-SQL providers map onto the SQL-oriented interface by convention (Redis getSchema() uses a non-blocking SCAN, never KEYS *) — read the provider doc before assuming a surface exists.
- Docker: multi-stage Bun build, standalone Next.js output; build args
JWT_SECRET_BUILD,ADMIN_PASSWORD_BUILD,USER_PASSWORD_BUILD. The Dockerfile declares noHEALTHCHECK—GET /api/db/healthis wired indocker-compose.example.ymland the chart probes. - Helm: lint with
helm lint charts/libredb-studio --strict. Values:charts/libredb-studio/README.md; rationale:docs/HELM_CHART.md.
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.