Skip to content

Latest commit

 

History

History
415 lines (354 loc) · 33.1 KB

File metadata and controls

415 lines (354 loc) · 33.1 KB

High-Level Architecture - LibreDB Studio

This document outlines the architectural patterns, tech stack, and system design for LibreDB Studio, a web-based SQL IDE for cloud-native teams.

System Overview

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.

1. Core Tech Stack

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)

2. High-Level Architecture Diagram

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
Loading

3. Database Provider Architecture

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
Loading

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.

4. Key Architectural Patterns

4.1. Strategy Pattern (Database & LLM)

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 type
  • src/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.

4.2. Authentication Flow

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
Loading

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.

4.3. Multi-Statement Execution

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 script grammar fact: an Oracle PL/SQL unit or a SQLite trigger is one statement, and a / (Oracle) or GO (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.

4.4. Storage Abstraction Layer

  • Write-through cache architecture: localStorage (L1 cache) + optional server storage (L2 persistent)
  • Three storage modes controlled by STORAGE_PROVIDER env var:
    • local (default): Browser localStorage only, zero configuration
    • sqlite: Server-side SQLite file via better-sqlite3
    • postgres: Server-side PostgreSQL via pg
  • useStorageSync hook 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_migrated flag prevents re-migration
  • Browser copy owner (server mode): libredb_workspace_owner binds 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

4.5. Client State Management

  • 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

4.6. Workspace Abstraction (npm package embedding)

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.tsx is 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.json exports/main/module point at the tsup dist/ output.
  • src/styles/theme.css — the semantic colour tokens every exported component resolves through, shipped as dist/styles.css (exports["./styles.css"]) because globals.css is not packaged. A host imports it once: import "@libredb/studio/styles.css". build:lib is tsup && node scripts/copy-theme.mjs in that order — tsup cleans dist/, so the copy has to follow it. See docs/ui/theming.md.
  • Platform integration rules (Tailwind tokens, Lucide stroke widths, chunk scanning) live in CLAUDE.md.

4.7. Standalone Boot Flow (src/instrumentation.ts)

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:

  1. Bootstraps missing auth env (src/lib/auth-bootstrap.ts, #109). When JWT_SECRET / ADMIN_PASSWORD are absent they are generated once, persisted to <data dir>/auth-bootstrap.json (mode 0600), and injected into process.env before any secret reader runs; the admin password is printed once. Explicitly set env vars always win. Disable with AUTH_BOOTSTRAP=off|false|0 (case-insensitive); an unrecognized value warns and stays on. In OIDC mode only the JWT secret is generated.
  2. Runs the auth-config preflight (src/lib/config/auth-preflight.ts, #227). A JWT_SECRET that 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/health is 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.
  3. Seeds the embedded LibreDB sample (src/lib/seed/libredb-sample.ts). Unless LIBREDB_EMBEDDED_SAMPLE=false, it creates <data dir>/sample.libredb (idempotently, atomic rename) and GET /api/connections/managed then advertises an editable, dismissable "Sample (LibreDB)" connection pointing at it.
  4. Seeds the embedded SQLite sample, asynchronously (src/lib/seed/sqlite-sample.ts). Unless SQLITE_EMBEDDED_SAMPLE=false, it fires-and-forgets a copy of the vendored seed-assets/sqlite/employee.db template 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 in pendingSeeds; useConnectionManager polls (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.

4.8. SQLite Driver Selection (src/lib/db/providers/sql/sqlite-driver.ts)

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.

4.9. Agent Runtime (src/lib/agent/, available when AI is configured)

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.

4.10. MCP Server (src/lib/mcp/, off by default)

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.

5. Directory Structure

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

6. Deployment

  • 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 to 0.0.0.0 where the namespace has no usable IPv6. HOSTNAME (chart: config.bindAddress) overrules that and is honoured verbatim. Canonical image ghcr.io/libredb/libredb-studio.
  • Native channels (bin/studio.js npx launcher, Homebrew tap, .deb/.rpm, Snap, standalone tarballs; sources under bin/ and packaging/): local-first, bind 127.0.0.1 by default unless --host or LIBREDB_BIND opts in, or a HOSTNAME that 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 in docs/DISTRIBUTION.md.
  • Health Check: GET /health, GET /api/health or GET /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.