This document outlines the architectural patterns, tech stack, and system design for LibreDB Studio, a web-based SQL IDE for cloud-native teams.
LibreDB Studio is a hybrid, cloud-native database management tool that provides an IDE-like experience in the browser.
It supports 28 database backends via a Strategy Pattern abstraction: PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, Db2 LUW, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Trino, Databend, Apache Cassandra, Elasticsearch, OpenSearch, Redis, Prometheus, InfluxDB (InfluxQL), InfluxDB 3 (SQL), Apache Kafka, etcd, Neo4j, Milvus, Qdrant, Oxia, LibreDB.
The count is the SHIPPED record in src/lib/db/compatibility.ts, which is exhaustive over DatabaseType; elasticsearch and opensearch are two ids served by one provider module, and influxdb and influxdb3 are two ids served by one provider directory with a class each.
It runs in two modes: as a standalone Next.js app and as an embedded npm package (@libredb/studio) consumed by libredb-platform. See §4.6.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) with React 19 |
| Runtime | Bun / Node.js |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS 4 + Shadcn/UI |
| Animations | Framer Motion v13 |
| SQL Editor | Monaco Editor |
| Data Grid | TanStack React Table + react-virtual |
| AI | Multi-model (Gemini, OpenAI, Ollama, Custom) |
| Auth | JWT (jose) + OIDC SSO (openid-client) |
| Charts | Recharts |
| Containerization | Docker (multi-stage Bun build) |
graph TD
User((User)) -->|HTTPS| Frontend[Next.js Frontend<br/>App Router + React 19]
Frontend -->|API Calls| API[Next.js API Routes<br/>src/app/api]
subgraph "Application Core"
API -->|Auth| AuthLib[src/lib/auth.ts<br/>JWT + OIDC]
API -->|Query| DBFactory[Provider Factory<br/>src/lib/db/factory.ts]
API -->|AI| LLMFactory[LLM Factory<br/>src/lib/llm/]
end
subgraph "Database Providers (Strategy Pattern)"
DBFactory --> SQL[SQL Providers]
DBFactory --> Document[Document Providers]
DBFactory --> KeyValue[Key-Value Providers]
DBFactory --> TimeSeries[Time-Series Providers]
DBFactory --> Stream[Stream Providers]
DBFactory --> Graph[Graph Providers]
DBFactory --> Vector[Vector Providers]
SQL --> PG[(PostgreSQL)]
SQL --> MySQL[(MySQL)]
SQL --> SQLite[(SQLite)]
SQL --> Oracle[(Oracle)]
SQL --> Db2[(Db2 LUW)]
SQL --> MSSQL[(SQL Server)]
SQL --> ClickHouse[(ClickHouse)]
SQL --> Druid[(Apache Druid)]
SQL --> Search[(Elasticsearch / OpenSearch)]
SQL --> Trino[(Trino)]
SQL --> Databend[(Databend)]
SQL --> Cassandra[(Apache Cassandra)]
SQL --> LibSQL[(libSQL)]
SQL --> DuckDB[(DuckDB)]
Document --> MongoDB[(MongoDB)]
Document --> Couchbase[(Couchbase)]
KeyValue --> Redis[(Redis)]
KeyValue --> Etcd[(etcd)]
KeyValue --> Oxia[(Oxia)]
TimeSeries --> Prometheus[(Prometheus)]
TimeSeries --> InfluxDB[(InfluxDB / InfluxDB 3)]
Stream --> Kafka[(Apache Kafka)]
Graph --> Neo4j[(Neo4j)]
Vector --> Milvus[(Milvus)]
Vector --> Qdrant[(Qdrant)]
DBFactory --> Embedded[Embedded Providers]
Embedded --> LibreDB[(LibreDB)]
end
subgraph "AI Providers (Strategy Pattern)"
LLMFactory --> Gemini[[Gemini]]
LLMFactory --> OpenAI[[OpenAI]]
LLMFactory --> Ollama[[Ollama]]
LLMFactory --> CustomLLM[[Custom]]
end
subgraph "Security"
AuthLib -->|Session| JWT[HTTP-Only JWT Cookies]
AuthLib -->|SSO| OIDC[OIDC Provider<br/>Auth0 / Keycloak / Okta / Azure AD]
end
classDiagram
class BaseDatabaseProvider {
<<abstract>>
+connect()
+disconnect()
+executeQuery()
+listContainers()
+countObjects()
+listObjects()
+describeObject()
+describeObjects()
+getHealth()
+getCapabilities() ProviderCapabilities
+getLabels() ProviderLabels
+prepareQuery() PreparedQuery
}
class SQLBaseProvider {
<<abstract>>
+beginTransaction()
+commitTransaction()
+rollbackTransaction()
+cancelQuery()
}
class GraphBaseProvider {
<<abstract>>
#profile GraphEngineProfile
+query()
+cancelQuery()
}
BaseDatabaseProvider <|-- SQLBaseProvider
BaseDatabaseProvider <|-- MongoDBProvider
BaseDatabaseProvider <|-- CouchbaseProvider
BaseDatabaseProvider <|-- RedisProvider
BaseDatabaseProvider <|-- PrometheusProvider
BaseDatabaseProvider <|-- InfluxDBProvider
BaseDatabaseProvider <|-- KafkaProvider
BaseDatabaseProvider <|-- EtcdProvider
BaseDatabaseProvider <|-- OxiaProvider
BaseDatabaseProvider <|-- GraphBaseProvider
BaseDatabaseProvider <|-- MilvusProvider
BaseDatabaseProvider <|-- QdrantProvider
BaseDatabaseProvider <|-- LibreDBProvider
SQLBaseProvider <|-- PostgresProvider
SQLBaseProvider <|-- MySQLProvider
SQLBaseProvider <|-- SQLiteProvider
SQLBaseProvider <|-- OracleProvider
SQLBaseProvider <|-- Db2Provider
SQLBaseProvider <|-- MSSQLProvider
SQLBaseProvider <|-- ClickHouseProvider
SQLBaseProvider <|-- DruidProvider
SQLBaseProvider <|-- SearchProvider
SQLBaseProvider <|-- TrinoProvider
SQLBaseProvider <|-- DatabendProvider
SQLBaseProvider <|-- CassandraProvider
SQLBaseProvider <|-- LibSQLProvider
SQLBaseProvider <|-- DuckDBProvider
SQLBaseProvider <|-- InfluxDB3Provider
GraphBaseProvider <|-- Neo4jProvider
Each provider implements:
getCapabilities()- queryLanguage, supportsExplain, supportsCreateTable, maintenanceOperations, etc.getLabels()- entityName, selectAction, searchPlaceholder, etc. (drives all UI text)prepareQuery()- handles query limiting per-provider (SQL LIMIT injection vs MongoDB native)
A graph engine is the one family with a shared base of its own: GraphBaseProvider (src/lib/db/graph/graph-base-provider.ts) runs every statement through the shared Cypher read policy, the engine's statement gate (which an allowlisted SHOW form skips) and one READ session over the Bolt transport, and the engine supplies a GraphEngineProfile (its policy lists, catalog reads, gate and error table) plus its declarations and monitoring reads.
The graph core under src/lib/db/graph/ is pure and shipped to the browser, where the editor's Cypher language and completion read it; bolt/ and the base class are server only.
ADDING_A_PROVIDER.md describes the layers and the profile, and providers/neo4j.md the one engine on them.
Adding a new database type requires: 1 provider class + 1 entry in db-ui-config.ts.
CouchbaseProvider extends BaseDatabaseProvider even though SQL++ is a SQL dialect: SQL++ quotes identifiers with doubled backticks, which escapeIdentifier() produces for no existing type, so it owns its quoting and declares its SQL-ness through queryLanguage: 'sql' instead. Being reached over HTTP is not the reason — ClickHouseProvider, DruidProvider and TrinoProvider add no driver either, and all three extend SQLBaseProvider, because double-quoted identifiers are correct in each dialect. Each driver-free provider is a directory rather than a single file, with its wire format behind a transport seam that provider logic never bypasses. Trino inherits everything except the limiter: its grammar is [ OFFSET count ] [ LIMIT count ] and only that way round, so prepareQuery() transposes the clause the shared limiter emits. See docs/providers/couchbase.md, clickhouse.md, druid.md and trino.md.
Both database and LLM layers use the Strategy Pattern with a factory:
src/lib/db/factory.ts- Creates the correct database provider based on connection typesrc/lib/llm/factory.ts- Creates the correct LLM provider based on configuration
No isMongoDB / === 'mongodb' checks outside provider classes. All behavior differences are driven through capabilities and labels.
src/lib/db/object-kinds.ts is the kernel through which every provider reads its own declaration, and it takes only facts that follow from the declaration and hold for every engine.
How deep the container chain is (containerDepth()), which container paths are an address (acceptedContainerShapes(), over containerPathShapes) and which object kinds exist (declaredKinds()) are such facts.
The HTTP object routes read them through the same functions, so a route refusal and a provider refusal cannot disagree about one declaration (#1147).
A rule that only one engine's reads need stays in that engine's file, next to the reads it protects.
PostgreSQL's containerSchema() is the example: it refuses a declaration that names no schema level, because the PostgreSQL reads look the schema up by id, so it lives beside those reads rather than in the kernel (#1092).
A descriptor field that only one engine sets is a sign that its rule belongs in that engine.
ObjectPathShapeEngine.attachedSegment (#978) is the one pre-existing exception: a per-engine acceptance policy carried in provider descriptors rather than in the declaration, and moving it into the declaration is separate work.
The editor's query dialects follow the same rule through three registries, each a Record the compiler holds complete.
QUERY_DIALECTS (src/lib/db/query-dialects.ts) gives each declared queryDialect its tab type and its three row-menu answers.
DIALECT_EDITORS (src/lib/editor/dialect-editors.ts) gives each tab type its Monaco language and its formatter, keyed by tab type because that is what a restored tab carries.
DIALECT_GENERATORS (src/lib/query-generators.ts) gives each dialect what a tree click and Generate Query write.
A dialect is one record in each, and no reader branches on its name: tests/unit/lib/dialect-reader-allowlist.test.ts holds every other reader of queryDialect, of the JSON language and of a negated language to a closed list with its owner.
sequenceDiagram
participant U as User
participant F as Frontend
participant A as API (/api/auth)
participant O as OIDC Provider
alt Local Auth
U->>F: Email + Password
F->>A: POST /api/auth/login
A->>F: Set HTTP-Only JWT Cookie
else Passkey sign-in
U->>F: Click Use a passkey
F->>A: POST /api/auth/passkey/sign-in {options}
A->>F: Challenge + signed ceremony cookie
U->>F: Unlock passkey on the device
F->>A: POST /api/auth/passkey/sign-in {verify}
A->>F: Set HTTP-Only JWT Cookie
else Platform launch
U->>F: Open the platform's launch link, token in the URL fragment
F->>A: POST /api/auth/launch {token}
A->>F: Set HTTP-Only JWT Cookie
else OIDC SSO
U->>F: Click SSO Login
F->>O: Redirect (PKCE)
O->>F: Authorization Code
F->>A: GET /api/auth/oidc/callback
A->>O: Token Exchange
A->>F: Set HTTP-Only JWT Cookie
end
Controlled by NEXT_PUBLIC_AUTH_PROVIDER (local | oidc). Every flow results in the same JWT session cookie. Proxy (src/proxy.ts) enforces RBAC (admin vs user roles).
The passkey branch exists only with local auth, STORAGE_PROVIDER=sqlite or postgres, and a valid PASSKEY_ORIGIN. The challenge travels in an HttpOnly, SameSite=Strict cookie signed with a key derived from JWT_SECRET, the verify step checks the assertion against the one configured origin and the credential stored in the server store, and marks the challenge spent in the same transaction that records the sign-in, so any replica completes it at most once. A verified passkey replaces the password and the TOTP code, because both ceremonies require user verification. See PASSKEYS.md.
The launch branch exists only while LAUNCH_TOKEN_SECRET is set.
The /launch page reads an HS256 token from its URL fragment, removes it from the address bar and posts it to POST /api/auth/launch: at once when the browser holds a session, otherwise only after the person clicks Continue on a page naming the account the token signs into.
The route verifies it against the configured issuer and audience, refuses a token issued for more than 60 seconds or presented twice, refuses to replace a session for another account, and in store mode creates or updates an account bound to the token's issuer and subject before it sets the session cookie; it never signs in to an account that has a password.
Under NEXT_PUBLIC_AUTH_PROVIDER=oidc the route and the page answer 503.
See LAUNCH.md.
src/lib/sql/statement-splitter.ts splits SQL input into individual statements, handling:
- String literals (single/double quotes)
- Block and line comments
- Dollar-quoting (PostgreSQL)
- Procedural bodies and separator lines, from the dialect's
scriptgrammar fact: an Oracle PL/SQL unit or a SQLite trigger is one statement, and a/(Oracle) orGO(SQL Server) line is a boundary that is never sent
Multi-statement queries execute sequentially via POST /api/db/multi-query, one request per execution unit (splitExecutionUnits): a statement, or on SQL Server the whole batch between GO lines.
- Write-through cache architecture: localStorage (L1 cache) + optional server storage (L2 persistent)
- Three storage modes controlled by
STORAGE_PROVIDERenv var:local(default): Browser localStorage only, zero configurationsqlite: Server-side SQLite file viabetter-sqlite3postgres: Server-side PostgreSQL viapg
useStorageSynchook in Studio.tsx: discovers mode at runtime via/api/storage/config, pulls on mount, pushes mutations (debounced 500ms)- Migration: First login auto-migrates localStorage to server;
libredb_server_migratedflag prevents re-migration - Browser copy owner (server mode):
libredb_workspace_ownerbinds the browser copy to the signed-in account; a different account starts from its own server data and sign-out clears the copy (see STORAGE.md) - Graceful degradation: If server unreachable, localStorage continues working
- Storage module (
src/lib/storage/) for persistent data: connections, query history, saved queries, schema snapshots, chart configs, audit log, masking config, threshold config - React hooks for UI state: tabs, active connection, execution status
- Custom hooks extracted from Studio.tsx:
useAuth,useConnectionManager,useTabManager,useTransactionControl,useQueryExecution,useInlineEditing
Studio ships both as a standalone app and as the @libredb/studio npm package consumed by libredb-platform (built with tsup via build:lib).
src/workspace/—StudioWorkspace.tsxis the embeddable shell. Its adapter hooks (hooks/use-connection-adapter,hooks/use-query-adapter) let the host (standalone or platform) supply connections and query execution, so the same UI runs in both contexts.src/exports/— barrel modules (components.ts,providers.ts,workspace.ts,types.ts) that define the package's public surface;package.jsonexports/main/modulepoint at the tsupdist/output.src/styles/theme.css— the semantic colour tokens every exported component resolves through, shipped asdist/styles.css(exports["./styles.css"]) becauseglobals.cssis not packaged. A host imports it once:import "@libredb/studio/styles.css".build:libistsup && node scripts/copy-theme.mjsin that order — tsup cleansdist/, so the copy has to follow it. Seedocs/ui/theming.md.- Platform integration rules (Tailwind tokens, Lucide stroke widths, chunk scanning) live in
CLAUDE.md.
Next.js runs register() once per server worker, only when Studio boots its own server (never when @libredb/studio is imported by libredb-platform) and only on the Node.js runtime. On standalone boot it:
- Bootstraps missing auth env (
src/lib/auth-bootstrap.ts, #109). WhenJWT_SECRET/ADMIN_PASSWORDare absent they are generated once, persisted to<data dir>/auth-bootstrap.json(mode0600), and injected intoprocess.envbefore any secret reader runs; the admin password is printed once. Explicitly set env vars always win. Disable withAUTH_BOOTSTRAP=off|false|0(case-insensitive); an unrecognized value warns and stays on. In OIDC mode only the JWT secret is generated. - Runs the auth-config preflight (
src/lib/config/auth-preflight.ts, #227). AJWT_SECRETthat is set but shorter than 32 characters prints an operator-facing banner (length only, never the value) and exits with code 1. It runs after bootstrap so a generated secret is validated too. This is the one step that intentionally stops boot:GET /api/db/healthis the Kubernetes livenessProbe and the Docker/PaaS health check, so signalling the failure there would restart the pod forever and hide the login screen's actionable 503; refusing to start costs nothing because a too-short secret can sign no session at all. - Seeds the embedded LibreDB sample (
src/lib/seed/libredb-sample.ts). UnlessLIBREDB_EMBEDDED_SAMPLE=false, it creates<data dir>/sample.libredb(idempotently, atomic rename) andGET /api/connections/managedthen advertises an editable, dismissable "Sample (LibreDB)" connection pointing at it. - Seeds the embedded SQLite sample, asynchronously (
src/lib/seed/sqlite-sample.ts). UnlessSQLITE_EMBEDDED_SAMPLE=false, it fires-and-forgets a copy of the vendoredseed-assets/sqlite/employee.dbtemplate to<data dir>/sample-employees.db(idempotent, atomic rename) — boot never waits. While the copy is in flight the managed-connections API advertises the seed id inpendingSeeds;useConnectionManagerpolls (1s, max 30) so "Sample (Employees)" appears without a page refresh.
Failures in the bootstrap and seeding steps are logged and swallowed — boot never breaks. The preflight in step 2 is the deliberate exception.
The SQLite DB provider is runtime-adaptive: it loads bun:sqlite under Bun and node:sqlite under plain Node (npx / brew / deb installs run node server.js). LIBREDB_SQLITE_DRIVER=bun|node forces a driver (used by tests). This is distinct from the storage layer, whose SQLite backend uses better-sqlite3.
A read-only investigation agent: a model drafts SQL against a connected database, repairs statements that fail, and composes a report whose claims cite the results they came from. Three boundaries define it. Its availability is derived, not flagged (#331 T5): the agent exists when a model is configured through the existing src/lib/llm settings and the durable ledger has a writable path, so no rail renders where the first Start would fail, and the discovery probe answers {"enabled": false, "reason": …} naming the condition that is missing — that is how the rail learns to stay absent and how the operator learns why. LIBREDB_AGENT_ENABLED=false remains the explicit off-switch. isAgentRuntimeEnabled() stays synchronous, answering the off-switch and the model configuration for its five in-request callers; the ledger's writable path is I/O and is composed into the answer by GET /api/agent/config alone. It is standalone-only, so the @libredb/studio package gains no agent module, agent type or runtime dependency (asserted by a package-boundary test); and every database reach goes through the same src/lib/db/operations/ pipeline as the rest of the app, under a read-only execution profile with the agent's own frozen policy — there is no second path to a driver. A run is an append-only ledger on a durable backend (WORKFLOW_TARGET_WORLD: zero-config single-instance local, or the opt-in Postgres world for multiple replicas), and it re-derives its state from that ledger, so a resumed run never repeats a tool execution. Model configuration is the existing src/lib/llm settings surface — there is no second place to enter a key, and therefore no second reader of one.
Full behaviour, the tool set, what bounds a run, the HTTP surface and the honest limitations: docs/AGENT.md.
An MCP endpoint at /api/mcp for AI clients of the user's own, served by the official MCP TypeScript SDK: createMcpHandler at module scope and one McpServer per request, in revision 2026-07-28 and, statelessly, 2025-11-25 and 2025-06-18.
It authenticates with a scoped bearer token each user mints on the settings screen, signed with a key derived from JWT_SECRET under a configured label, and never with the session cookie; src/proxy.ts and the route both check the Origin, the Host on a loopback bind, and the token.
Its three tools reach only seed connections opted in with mcp: true, through acquireExecutionProfileProvider alone, bound every result to 32 KiB behind an untrusted-content notice, and write mcp_operation audit events, the decision before any provider.
It is standalone-only: nothing under src/lib/mcp/ or src/app/ is reachable from the package's entry points, which a package-boundary test asserts.
Full behaviour, client configuration and limits: docs/MCP.md.
src/
├── app/ # Next.js App Router
│ ├── api/
│ │ ├── auth/ # Login/logout/me + OIDC (PKCE, callback), TOTP, passkey/ (owner management) + passkey/sign-in/
│ │ ├── ai/ # explain, query-safety, describe-schema
│ │ ├── db/ # Query, objects/ (the object surface), health, maintenance, transactions
│ │ ├── storage/ # Storage sync API (config, CRUD, migrate)
│ │ ├── connections/ # managed/ — built-in (seeded) connections listing
│ │ │ # policy/: whether this server allows custom connections
│ │ ├── agent/ # Agent runs, stream, artifacts, drive (404 unless enabled — §4.9)
│ │ ├── mcp/ # MCP endpoint (bearer token, 404 unless enabled) and token/ (minting)
│ │ └── admin/ # Fleet health, audit
│ ├── admin/ # Admin dashboard (RBAC protected) — layout.tsx renders the
│ │ │ # shell; one route per section, each independently
│ │ │ # linkable/refreshable. `/admin` redirects to the default
│ │ │ # section and maps legacy `?tab=` links (src/lib/admin-sections.ts)
│ │ ├── overview/ # Fleet health, quick actions
│ │ ├── operations/ # Maintenance operations
│ │ ├── monitoring/ # Embedded monitoring dashboard
│ │ ├── security/ # Data masking, access control
│ │ └── audit/ # Audit log
│ ├── monitoring/ # Monitoring dashboard page
│ └── login/ # Login page
├── components/
│ ├── Studio.tsx # Main application shell (standalone)
│ ├── QueryEditor.tsx # Monaco SQL editor wrapper
│ ├── ResultsGrid.tsx # Virtualized data grid
│ ├── SchemaDiagram.tsx # React Flow ERD viewer
│ ├── agent/ # AgentRail + timeline/hydration folds (standalone only — §4.9)
│ ├── sidebar/ # ConnectionsList, ConnectionItem
│ ├── studio/ # StudioTabBar, QueryToolbar, BottomPanel
│ ├── results-grid/ # ResultCard, RowDetailSheet, StatsBar
│ ├── admin/ # AdminDashboard shell (section routes) + tabs/ panels
│ ├── monitoring/ # MonitoringDashboard + tabs
│ ├── object-tree/ # The desktop sidebar's lazy object tree (containers, folders, objects, columns)
│ │ ├── ObjectTree.tsx # Tree shell: hand-rolled window, roving tabindex, keyboard, menu anchor
│ │ ├── TreeRow.tsx # One row, ARIA numbers taken verbatim; the chevron is its own hit target
│ │ ├── RowMenu.tsx # The row menu, rendered as a sibling of the tree element, not inside it
│ │ ├── flatten.ts # Expansion state to a flat row list, with each row's ARIA position (pure)
│ │ ├── use-tree-nodes.ts # The lazy cache: containers, counts, a folder's objects, a row's columns
│ │ ├── row-actions.ts # What a row may be asked to do, read off the kind's own declaration
│ │ └── index.ts # What a shell imports: ObjectTree plus the two types its handlers need
│ ├── schema-explorer/ # SchemaExplorer (the flat list: mobile schema tab, published export)
│ └── ui/ # Shadcn/UI primitives
├── workspace/ # Embeddable shell (StudioWorkspace) + host adapter hooks
├── exports/ # Public npm-package barrel exports (tsup build:lib)
├── hooks/ # Custom React hooks
└── lib/
├── db/ # Database provider module
│ ├── providers/
│ │ ├── sql/ # postgres, mysql, sqlite (+ sqlite-driver runtime adapter), oracle, db2/ (driver seam + SYSCAT catalog over db2-node), mssql, clickhouse/ (transport seam + SQL over HTTP), druid/ (transport seam + SQL over POST /druid/v2/sql), search/ (transport seam + SQL over HTTP; elasticsearch and opensearch, two ids one module), trino/ (transport seam + SQL over the Trino client protocol), databend/ (transport seam + SQL over Databend's HTTP query API), cassandra/ (transport seam + CQL over the native protocol via cassandra-driver), libsql/ (transport seam + SQLite's dialect over the Hrana protocol), duckdb/ (driver seam + an embedded analytical engine over @duckdb/node-api)
│ │ ├── document/ # mongodb, couchbase/ (transport seam + SQL++ over REST)
│ │ ├── keyvalue/ # redis, etcd/ (gRPC client seam + an etcdctl subset over etcd's gRPC API via the shared gRPC transport), oxia/ (gRPC client seam + an oxia client read-command console over Oxia's gRPC client API via the shared gRPC transport; key order probed)
│ │ ├── timeseries/ # prometheus/ (transport seam + PromQL over the Prometheus HTTP API); influxdb/ (influxdb and influxdb3, two classes on two bases
│ │ │ # sharing one connection layer over the shared node transport: InfluxQL over the v1 /query API, and SQL over
│ │ │ # InfluxDB 3's /api/v3/query_sql; the InfluxQL lexer, read policy, quoter and generators are browser-safe)
│ │ ├── stream/ # kafka/ (read-client seam + JSON read requests over the Kafka protocol via @platformatic/kafka)
│ │ ├── graph/ # neo4j/ (an engine profile, catalog, statement gate and monitoring on the graph layer below)
│ │ ├── vector/ # milvus/ (a gRPC client of its own via the shared gRPC transport, Milvus's REST v2 requests as the console, run over gRPC); qdrant/ (a REST client of its own over the shared node transport, the closed console, the payload sample)
│ │ └── embedded/ # libredb (built-in embedded provider for the sample connection)
│ ├── graph/ # The graph layer a Cypher-over-Bolt engine extends (docs/ADDING_A_PROVIDER.md, "Adding a graph engine"):
│ │ # cypher/ (lexer, statements, quoting, read policy, generators), objects.ts, values.ts and
│ │ # profile.ts are pure and browser-safe; bolt/ (the GraphClient seam, the URI, the one
│ │ # neo4j-driver-lite client, driver values to JSON) and graph-base-provider.ts are server only
│ ├── http/ # endpoint.ts: the validated URL builder every HTTP transport uses (no redirects); node-transport.ts: the shared node:http(s) transport a new REST provider takes (one keep-alive Agent per connection, no proxy variables, a streamed byte cap)
│ ├── grpc/ # channel.ts: the one gRPC channel (options, unary and bidirectional calls, deadlines, aborts, the sent or unsent notice); credentials.ts: TLS credentials and the closing wrapper; tls.ts: the SSL / TLS panel, the TLS identity and the dial target, for every gRPC provider
│ ├── factory.ts # Provider factory
│ ├── query-dialects.ts # The dialect registry: each queryDialect's tab type and row-menu answers
│ └── types.ts # Database types
├── agent/ # Agent runtime: run ledger, workflow, tools, policy (docs/AGENT.md)
├── mcp/ # MCP server: SDK handler, token, pre-processing, tools (docs/MCP.md)
├── passkey/ # Passkey sign-in (docs/PASSKEYS.md): config (PASSKEY_ORIGIN reader), policy, ceremony
│ # cookie, WebAuthn wrapper, management and sign-in services, browser client
├── launch/ # Launch-token sign-in (docs/LAUNCH.md): config (LAUNCH_TOKEN_* reader), the token
│ # verifier (jose, HS256 pinned) and the in-process jti replay cache
├── llm/ # LLM provider module
├── editor/ # Monaco completions (SQL + MongoDB), the tab-type/language ladder, the
│ # editor registry (dialect-editors.ts), the LibreDB, Redis, etcd and Oxia command languages, Cypher, InfluxQL, and the Milvus and Qdrant console languages
├── schema-diff/ # Diff engine + migration SQL generator
├── export/ # The writers behind every "save this to disk": RFC 4180 CSV,
│ # the SQL INSERT/DDL forms, and the one blob-download path
├── sql/ # Statement splitter, alias extractor
├── seed/ # Seed connections: operator sources (sources/), operator loader, filter, credential resolver, CapRover discovery + sample seeding
├── config/ # auth-env.ts — single JWT_SECRET reader (auth.ts, proxy.ts, oidc.ts)
│ # custom-connections.ts: the ALLOW_CUSTOM_CONNECTIONS switch
├── api/ # API error codes + object-route helpers
├── ssh/ # SSH tunnel support
├── auth.ts # JWT utilities
├── auth-bootstrap.ts # Zero-config first-run auth bootstrap (runs in instrumentation)
├── oidc.ts # OIDC utilities
└── storage/ # Storage abstraction layer
├── index.ts # Barrel export
├── storage-facade.ts # Public sync API + CustomEvent dispatch
├── local-storage.ts # Pure localStorage CRUD
├── factory.ts # Env-based provider factory
└── providers/ # SQLite + PostgreSQL backends
- Docker / Helm: Multi-stage Bun build with standalone Next.js output; these channels resolve their bind address in the container entrypoint, preferring a dual-stack
::that they verify by connecting an IPv4 client to a throwaway listener, and falling back to0.0.0.0where the namespace has no usable IPv6.HOSTNAME(chart:config.bindAddress) overrules that and is honoured verbatim. Canonical imageghcr.io/libredb/libredb-studio. - Native channels (
bin/studio.jsnpx launcher, Homebrew tap,.deb/.rpm, Snap, standalone tarballs; sources underbin/andpackaging/): local-first, bind127.0.0.1by default unless--hostorLIBREDB_BINDopts in, or aHOSTNAMEthat differs from the machine's own name does (resolveBindAddress, #813: an inherited value is what a shell or a container runtime exported, not a choice). The npx launcher ships as a pure library and downloads the SHA256-verified standalone server tarball from GitHub Releases. Full matrix and per-channel details indocs/DISTRIBUTION.md. - Health Check:
GET /health,GET /api/healthorGET /api/db/health— same answer, no dependencies - Stateless API: API routes are stateless, suitable for horizontal scaling
- Environment: Configured via
.env.local(see CLAUDE.md for full variable list). Missing auth secrets are generated on first standalone boot — see §4.7.