Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tests
playwright-report
test-results
data-e2e-offline
data-e2e-base-path
.loop
loop
.claude
Expand Down
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
#
# ============================================

# ============================================
# BUILD-TIME SUBPATH (Optional)
# ============================================
# For /libredb, /tools/libredb or /~/libredb, build your own image/app with this
# value. It cannot relocate a prebuilt image at runtime. No trailing slash.
# BASE_PATH=/tools/libredb
# Details, reverse-proxy rules and OIDC callback URLs: docs/SUBPATH.md

# ============================================
# SERVER BIND ADDRESS (Optional)
# ============================================
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ jobs:
USER_EMAIL: user@libredb.org
USER_PASSWORD: test-user

- name: Run subpath deployment E2E
# Separate build, after the root-path servers have stopped.
run: bun run test:e2e:base-path

- name: Upload Playwright report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,6 @@ deploy/digitalocean/droplet/scripts/99-img-check.sh
/probe-*.yaml
/probe-results/
/probe-results*.json

# Isolated data for the production subpath regression suite.
data-e2e-base-path/
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV DOCKER_BUILD=true

# Next.js bakes this prefix into routes and browser bundles. Rebuild to change it.
ARG BASE_PATH=""

ARG JWT_SECRET_BUILD="build-time-placeholder-secret-32ch"
ARG ADMIN_PASSWORD_BUILD="build"
ARG USER_PASSWORD_BUILD="build"
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,9 @@ Deploy your own instance of LibreDB Studio with a single click on DigitalOcean,

## Deployment (DevOps)

For a reverse-proxy path such as `/tools/libredb`, build with `BASE_PATH` and follow the
[subpath deployment guide](docs/SUBPATH.md). Prebuilt images use the root path.

> Maintainers: every distribution channel is inventoried in
> [`distribution/channels.yaml`](distribution/channels.yaml); `bun run distribution:check`
> reports version drift across all of them (see
Expand Down
10 changes: 2 additions & 8 deletions charts/libredb-studio/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: libredb-studio
description: Web-based SQL IDE for cloud-native teams supporting sixteen engines - PostgreSQL, MySQL, SQLite, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra and libSQL
type: application
version: 0.1.61
version: 0.1.62
appVersion: "0.15.0"
kubeVersion: ">=1.26.0-0"
home: https://github.com/libredb/libredb-studio
Expand Down Expand Up @@ -113,13 +113,7 @@ annotations:
- name: Source
url: https://github.com/libredb/libredb-studio
artifacthub.io/changes: |
- "The OpenShift operator bundle now reaches both community catalogs on every release. Submissions to operatorhub.io and the OpenShift console catalog were hand-made until now, which is why both sat at 0.9.59 while the product reached 0.14.1 and 0.14.0 was never submitted at all"
- "A database size the server declines to measure is reported as absent rather than as 0 bytes, on MySQL, PostgreSQL, SQL Server and Oracle. An unmeasured database no longer reads as an empty one in the Overview panel"
- "CREATE TABLE emits the DDL of the engine it is aimed at instead of PostgreSQL's, so the generated statement runs where it was generated"
- "Agent grounding no longer captures engine and extension objects, so a run is grounded in the schema the question is about"
- "An agent stop that read nothing is answered for whoever stopped it rather than left silent"
- "The bundled runtime moves to Bun 1.4.2, with the in-range dependency groups"
- "Track app release 0.15.0 (appVersion bump; default image tag follows)"
- "Prefix default health probes with config.basePath for images built to run under a subpath"
dependencies:
- name: postgresql
version: "16.x.x"
Expand Down
14 changes: 13 additions & 1 deletion charts/libredb-studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ helm install libredb libredb/libredb-studio \

```bash
helm install libredb oci://ghcr.io/libredb/charts/libredb-studio \
--version 0.1.61 \
--version 0.1.62 \
--set secrets.jwtSecret=$(openssl rand -base64 32) \
--set secrets.adminPassword=MyAdmin123
```
Expand Down Expand Up @@ -391,6 +391,7 @@ would otherwise lose reachability. One line in the pod log names the address it
`config.bindAddress` overrules the resolver when you would rather state it than leave it to the
image:

| `config.basePath` | Must match `BASE_PATH` baked into your custom image; prefixes default health probes | `""` |
| `config.bindAddress` | Effect |
|---|---|
| `""` (default) | the container resolves it, preferring a verified dual-stack `::` |
Expand Down Expand Up @@ -639,3 +640,14 @@ fixed `runAsUser`/`runAsGroup`/`fsGroup` so the SCC can assign valid IDs;
UIDs: every writable path is a volume mount. Set `force` to always adapt (for
example when templating manifests offline for an OpenShift cluster) or
`disabled` to keep the fixed IDs everywhere.

## Deployment under a subpath

Build a custom image with `docker build --build-arg BASE_PATH=/tools/libredb ...`, then set
`config.basePath: /tools/libredb` and the matching image repository/tag. This prefixes the default
startup, readiness and liveness probes. It does not change routes in an already-built image.
Explicit custom probe paths are preserved. Set Ingress paths or HTTPRoute matches to the same
prefix and preserve it when forwarding; do not strip or rewrite it.

See [subpath deployment](https://github.com/libredb/libredb-studio/blob/main/docs/SUBPATH.md)
for complete build, reverse-proxy and OIDC examples.
9 changes: 9 additions & 0 deletions charts/libredb-studio/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,12 @@ Return the PostgreSQL URL when subchart is enabled
{{- define "libredb-studio.postgresql.url" -}}
{{- printf "postgresql://%s:$(POSTGRES_PASSWORD)@%s:5432/%s" .Values.postgresql.auth.username (include "libredb-studio.postgresql.fullname" .) .Values.postgresql.auth.database }}
{{- end }}

{{/* Prefix only the shipped health path; preserve explicit HTTP/exec/TCP probes. */}}
{{- define "libredb-studio.probe" -}}
{{- $probe := deepCopy .probe -}}
{{- if and $probe.httpGet (eq ($probe.httpGet.path | default "") "/api/db/health") -}}
{{- $_ := set $probe.httpGet "path" (printf "%s/api/db/health" .basePath) -}}
{{- end -}}
{{- toYaml $probe -}}
{{- end -}}
6 changes: 3 additions & 3 deletions charts/libredb-studio/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,15 @@ spec:
{{- end }}
{{- with .Values.startupProbe }}
startupProbe:
{{- toYaml . | nindent 12 }}
{{- include "libredb-studio.probe" (dict "probe" . "basePath" ($.Values.config.basePath | default "")) | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- include "libredb-studio.probe" (dict "probe" . "basePath" ($.Values.config.basePath | default "")) | nindent 12 }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- include "libredb-studio.probe" (dict "probe" . "basePath" ($.Values.config.basePath | default "")) | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
Expand Down
6 changes: 6 additions & 0 deletions charts/libredb-studio/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@
"config": {
"type": "object",
"properties": {
"basePath": {
"type": "string",
"description": "Build-time BASE_PATH of the custom image; prefixes default probes, not a runtime app setting",
"pattern": "^$|^(/[A-Za-z0-9._~-]+)+$",
"not": { "pattern": "(^|/)\\.\\.?(/|$)" }
},
"logLevel": {
"type": "string",
"enum": ["debug", "info", "warn", "error"],
Expand Down
3 changes: 3 additions & 0 deletions charts/libredb-studio/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ secrets:
authProvider: "local"

config:
# -- Must match BASE_PATH baked into a custom image. Prefixes default health probes;
# this value cannot change the routes in a prebuilt image. Empty means root.
basePath: ""
# -- Log level
logLevel: "info"
# -- Address the container listens on, written to HOSTNAME. Empty is the
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ services:
# - ./seed-connections.yaml:/app/config/seed-connections.yaml:ro

# Image runs as node:24.16.0-trixie-slim (no curl/wget) — use Node's built-in fetch.
# BASE_PATH must match a custom image built with that prefix (docs/SUBPATH.md).
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/db/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
test: ["CMD", "node", "-e", "fetch('http://localhost:3000${BASE_PATH:-}/api/db/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
BASE_PATH: ${BASE_PATH:-}
ports:
- "3000:3000"
environment:
Expand Down
35 changes: 1 addition & 34 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ None of it is a GitHub issue.
- [Dependencies](#dependencies) — P1–P5 · 5
- [Documentation](#documentation) — DOC3, DOC4 · 2
- [Release pipeline](#release-pipeline) — REL1–REL3 · 3
- [Chart configuration surface](#chart-configuration-surface) — N1, N3 · 2
- [Chart configuration surface](#chart-configuration-surface) — N1 · 1
- [Security Phase 1 deferrals](#security-phase-1-deferrals) — H1–H8 · 2
- [Security Phase 2 deferrals](#security-phase-2-deferrals) — C3–C11 · 7
- [Security Phase 3 deferrals](#security-phase-3-deferrals) — K4
Expand Down Expand Up @@ -988,39 +988,6 @@ Note the naming collision: `route.*` in `values.yaml` means Gateway API as of #3
**Done when:** an OpenShift cluster can be served by the chart alone, with TLS termination selectable,
and the README says which of the three exposure mechanisms belongs to which platform.

### N3. Subpath deployment is build-time only, which is why #369 is deferred rather than scheduled

[#369](https://github.com/libredb/libredb-studio/issues/369) asks to serve Studio under a path prefix
on a shared domain — `https://example.com/libredb` next to `https://example.com/grafana`.
`next.config.ts` sets no `basePath` and no `assetPrefix`, so there is zero support today.

The constraint, recorded so nobody rediscovers it: **Next.js `basePath` is baked at build, not read at
runtime.** Asset URLs (`/_next/static/...`) are emitted into the HTML and JS at build time and there
is no supported runtime override. So a `BASE_PATH` env var on the prebuilt image cannot work — the
feature has to be a build arg and a rebuilt image.

A reverse-proxy `StripPrefix` is not a workaround either. The browser asks for `/libredb/`, the proxy
strips it, the app answers with HTML referencing `/_next/static/...` at the root, and that follow-up
request no longer matches the `/libredb` router rule. Grafana can do this at runtime because it is a
Go server templating its own HTML; a statically built Next.js app is structurally different.

The surface a build-time implementation touches: roughly 40 `fetch('/api/...')` call sites, roughly 15
`router.push('/...')`, the cookie `path: "/"` in `src/lib/auth.ts` and the OIDC login route, OIDC
redirect URIs, the `src/proxy.ts` matcher, the Docker healthcheck, the chart's ingress and route
paths, the npm library surface, the E2E suite and the docs of roughly 27 distribution channels.
`next/link` and the app-router `router` prefix automatically; `fetch`, middleware redirects and cookie
paths do not.

Deferred rather than scheduled because the acquisition-relevant PaaS one-click listings hand out
subdomains, not subpaths, so no shipped channel needs it.

Related sharp edge, same silent-no-op class as #366: `values.yaml` already lets a user set
`ingress.hosts[].paths[].path` to `/libredb`, the install succeeds, and the app is unreachable.

**Done when:** a `BASE_PATH` build arg produces an image reachable under a path prefix — assets, API
calls, auth cookie and OIDC redirect included — verified against a real path-routing proxy, or the
chart refuses a non-root ingress path outright.

---

## Security Phase 1 deferrals
Expand Down
126 changes: 126 additions & 0 deletions docs/SUBPATH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Deploy Studio under a subpath

Studio can run at `/libredb`, `/tools/libredb`, or `/~/libredb` behind a reverse proxy.
Set `BASE_PATH` **when building**. Next.js bakes this prefix into routes and browser bundles;
setting it only on a prebuilt image cannot relocate that image. The published images use `/`.

Use a leading slash and no trailing slash. Empty or `/` selects the root. Path segments may
contain letters, digits, `.`, `_`, `~`, and `-`; dot segments, encoded paths, query strings,
fragments, backslashes, and repeated slashes are rejected during the build.

## Build from source

Keep the same value for build and `next start`:

```sh
export BASE_PATH=/tools/libredb
bun install --frozen-lockfile
bun run build
bun start
```

With Docker, build a custom image from the source revision containing subpath support:

```sh
docker build --build-arg BASE_PATH=/tools/libredb -t studio-subpath:local .
docker run --rm -p 3000:3000 --env-file .env.local studio-subpath:local
```

The build uses the prefix from the build arg. Supply your normal runtime authentication,
storage, and LLM settings as usual. `BASE_PATH` is not a replacement for a public origin or
for `ALLOWED_ORIGINS`; that setting still takes origins such as `https://example.com`.

The source `docker-compose.yml` forwards `BASE_PATH` as a build arg:

```sh
BASE_PATH=/tools/libredb docker compose up --build
```

When using `docker-compose.example.yml`, select your custom image and set the same `BASE_PATH`
in Compose's environment so its health check uses the built path.

## Preserve the prefix at the reverse proxy

Forward `/tools/libredb` and everything below it **with the path intact**. Do not use
Traefik `StripPrefix`, an Nginx rewrite, or an HTTPRoute `URLRewrite` to remove the prefix.
For example, an Nginx upstream without a URI suffix preserves the original path:

```nginx
location ~ ^/tools/libredb(?:/|$) {
proxy_pass http://studio:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
}
```

For Traefik, match ``PathPrefix(`/tools/libredb`)`` and forward to Studio without a strip-prefix
middleware. Restrict the rule to your intended hostname as usual. Assets, API calls,
streamed agent responses, and native authentication redirects all use the configured prefix.
Next's router and `Link` apply it automatically. Studio has no WebSocket endpoint to configure.

## Helm and Gateway API

Build and publish your own image with the prefix first. Then use matching chart values:

```yaml
image:
repository: registry.example.com/studio-subpath
tag: my-build
config:
basePath: /tools/libredb

ingress:
enabled: true
hosts:
- host: example.com
paths:
- path: /tools/libredb
pathType: Prefix
```

`config.basePath` prefixes the chart's default startup, readiness, and liveness health paths.
It does not set a runtime app environment variable. Explicit custom probe paths, exec probes,
and TCP probes are preserved. A mismatched image and chart prefix fails readiness.

For a Gateway API deployment, use an HTTPRoute match with the same prefix and your cluster's
actual Gateway reference:

```yaml
route:
main:
enabled: true
parentRefs:
- name: my-gateway
hostnames:
- example.com
matches:
- path:
type: PathPrefix
value: /tools/libredb
```

Do not add a rewrite filter. If you use external health checks, request
`/tools/libredb/api/db/health`.

## Authentication and editor assets

Register the full OIDC callback URL, for example
`https://example.com/tools/libredb/api/auth/oidc/callback`, and allow the logout return URL
`https://example.com/tools/libredb/login` at your identity provider. Session and OIDC state
cookies use `/tools/libredb` as their path, including when they are removed.

The default Monaco asset URL becomes `/tools/libredb/monaco/vs`. An explicit
`NEXT_PUBLIC_MONACO_VS_PATH` override is used exactly as configured, including an external
asset origin; do not add the prefix a second time.

For the embedded npm library, the host application's routing remains its responsibility.
Standalone builds publish the internal `NEXT_PUBLIC_BASE_PATH` value from `BASE_PATH`; it is
not a separate operator setting.

## Verification

`bun run test:e2e:base-path` builds at `/~/libredb`, starts a local path-preserving proxy that
returns 404 outside that prefix, and tests login, RBAC, cookie deletion, a real SQLite query, origin checks, OIDC error
redirects, and self-hosted editor assets in Chromium. Run it after the ordinary E2E suite;
the two configurations build different versions of `.next`.
Loading
Loading