Studio
npx turbine studio launches a local web UI for browsing your database and composing queries visually. It is read-only by default: no row editing, no DDL, no raw-SQL input surface, and every query executes inside a BEGIN READ ONLY transaction. --write opts a single launch in to primary-key-addressed edits through the same validated builder; without the flag, the write endpoints do not exist.
DATABASE_URL=postgres://user:pass@localhost:5432/mydb npx turbine studioStudio introspects the schema, starts an HTTP server on 127.0.0.1:4983 (Node's built-in http module, no extra dependencies), and opens your browser with a per-process session token in the URL.
Try it without a database#
--demo boots Studio against a seeded, in-memory sample database. No connection string, no setup:
npx turbine-orm@latest studio --demoThe sample dataset (users, posts, comments, orgs, with relations and two PII-tagged columns) is served from Turbine's SQLite engine over node:sqlite's :memory: database, so it needs no DATABASE_URL and adds no dependency. Requires Node 22.5+.
A Read-only / Show PII / Write switcher in the demo banner toggles PII redaction and writes live, so you can try the redaction behavior and the write flow back to back. Every launch boots read-only with PII redacted. Writes apply to the in-memory store (edits stick, a refresh shows them), but nothing is ever saved anywhere: the store dies with the process and every launch starts pristine.
Launching#
npx turbine studio # default: 127.0.0.1:4983, opens browser, read-only
npx turbine studio --port 5173 # custom port
npx turbine studio --no-open # don't auto-open the browser
npx turbine studio --schema inventory # non-public Postgres schema
npx turbine studio --include users,posts # only these tables
npx turbine studio --exclude _migrations # hide tables
npx turbine studio --write # opt in to PK-addressed writes (loud warning)
npx turbine studio --show-pii # reveal PII-tagged values (loud warning)
npx turbine studio --demo # seeded in-memory sample DB (no DATABASE_URL)
npx turbine studio --host 0.0.0.0 --allow-remote # non-loopback requires explicit opt-inThe connection string resolves the same way as every other CLI command: --url flag, then DATABASE_URL, then turbine.config.ts. See CLI, config resolution.
| Flag | Description |
|---|---|
--port <n> | HTTP port (default: 4983). |
--host <addr> | Bind address (default: 127.0.0.1). Non-loopback values are refused unless you also pass --allow-remote. |
--allow-remote | Opt in to binding on a non-loopback --host. Emits a loud warning and proceeds. Without it, Studio exits with code 1. |
--write | Enable insert/update/delete from the Data tab: single rows, multi-select delete, and paste-to-insert batches, every row addressed by its full primary key. Emits a startup warning; the UI shows a persistent WRITE MODE banner. |
--show-pii | Show PII-tagged column values instead of the redaction placeholder. Emits a startup warning. |
--demo | Seeded in-memory sample database; nothing is saved. Enables the in-UI Read-only / Show PII / Write switcher. Requires Node 22.5+. |
--no-open | Don't open the browser automatically. |
--include <a,b> / --exclude <a,b> | Limit which tables Studio sees. |
--schema <name> | Postgres schema (default: public). Pinned via set_config('search_path', ...) on every query. |
The Query tab, a visual findMany builder#
The Query tab (the default) composes real Turbine queries, not SQL. You pick a table and build up findMany arguments:
- Fields,
select/omitcolumn picks. - Filters, the full
whereoperator set (equals,not,in,gt/gte/lt/lte,contains,startsWith,endsWith,IS NULL/IS NOT NULL, case-insensitive mode) withAND/OR/NOTgrouping. Incomplete clauses explain themselves instead of silently disabling Run. - Order + limit,
orderByon any column,limiton the result. - Relations, drill into
withrecursively, to any depth, choosing fields, filters, ordering, and limits at every level.
A live preview shows the exact db.table.findMany({ ... }) call your builder state compiles to, and Copy TS puts it on your clipboard, so a query you prototype in Studio pastes straight into your codebase. After a run, View SQL shows the single statement the query compiled to, and Copy SQL copies it.
Because the builder speaks ORM args instead of SQL, the server can validate everything: POST /api/builder checks every identifier (table, relation, field, orderBy column) against the introspected schema and compiles the query with the same QueryInterface.buildFindMany the library uses, every value bound as a $N parameter. There is no string of SQL anywhere in the request.
Data and Schema tabs#
- Data, browse table rows with column sorting, search across every text column, per-column filters (
equals,not,contains, comparisons, null checks; stackable, all parameterized server-side), and a modal viewer for JSON/JSONB cells. Select rows with the checkbox column to copy or export them (JSON/CSV); double-click any cell to copy its value. Page size is adjustable up to 500 rows. In write mode, this tab also hosts the row editor and batch tools. - Schema, inspect tables, columns, types, indexes, and relations as introspected. Columns tagged
pii: truecarry a pii badge.
Both tabs go through the same parameterized query path as the builder; reads stay inside BEGIN READ ONLY even in write mode.
Zone-less timestamp and date cells render as UTC, using the same parsers the client registers (from one shared helper, so the two cannot drift). A cell therefore shows the same instant your application reads, and in --write mode an edit echoes the same value back. turbine mcp applies the same parsers to the rows it samples. See Zone-less columns.
Write mode (--write)#
By default there is nothing to opt out of: the write endpoints are not registered, requests to them 404, and every transaction is read-only at the database level. --write changes that for a single launch:
- The Data tab gains Insert row, Edit, Delete, multi-select Delete selected, and Paste rows (bulk insert from pasted TSV/CSV with a header row, or a JSON array). The UI shows a persistent red WRITE MODE banner.
- Every write is addressed by a full primary key: a single row, or a capped batch (max 500) of PK-addressed statements in one all-or-nothing transaction. No predicate-based mutation, no bulk update, no DDL, no SQL input surface: writes compile through the same validated
buildUpdate/buildCreate/buildDeletebuilders as the library, identifiers checked against the introspected schema, values bound as$Nparameters. - Each write request runs in its own transaction with the same parameterized
statement_timeoutand pinnedsearch_pathas reads. A batch where any row fails, or any PK matches nothing, rolls back entirely. - The row editor is typed: enum and boolean columns get dropdowns, JSON columns validate before submit, timestamps get ISO 8601 placeholders.
- Write requests additionally require a matching
Originheader; absent or mismatched origins are rejected.
Write mode is for fixing a few bad rows without opening psql, not for data entry. Tables without a primary key are not editable. Views are not shown at all: Studio introspects tables only, so a view never appears in the sidebar or the schema API (--include-views is a pull / generate flag, and studio has no equivalent).
PII redaction#
Columns tagged pii: true in defineSchema render as a redaction placeholder in every tab: table rows, builder results, nested relation rows, and the echoed row after a write. Redaction happens server-side, before the value is serialized into the API response, so the browser never receives the value. Redacted columns are also excluded from the Data tab's substring search, per-column filters (even IS NULL, since null-ness is information), and sorting, so a hidden value cannot be inferred those ways; their headers say so on hover. --show-pii turns redaction off for a launch, with a startup warning and a persistent in-page banner.
One nuance in write mode: the row echoed immediately after a write never contains PII values regardless of --show-pii, because the write statement excludes those columns at the SQL level; the Data grid shows them on its next refresh. See PII fields for how the same tags behave in application queries.
Saved queries and the command palette#
Queries you save persist to .turbine/studio-queries.json in your project (commit it to share a query library with your team). In --demo mode saved queries live in memory only. Saved queries appear in the sidebar and in the Cmd+K command palette, which also jumps to any table or tab in one keystroke.
Keyboard shortcuts: Cmd+Enter runs the current query, Cmd+S opens the Save dialog for it (the dialog's Save button confirms; the browser's own save dialog is suppressed), G then Q / D / S switches tabs, R refreshes the Data tab, Shift+R reloads the schema, / focuses the sidebar filter.
Saved queries are builder-state only. Entries saved as raw SQL by old Studio versions are ignored on load, with a console notice; Studio has no way to execute them.
Security model#
Each layer assumes the layer above it failed:
- Loopback binding. Binds
127.0.0.1by default and refuses non-loopback--hostvalues unless you pass--allow-remote. Studio is a single-user local tool, not a deployable service. - Per-process token auth. A random 24-byte (192-bit) hex token is generated at startup and required on every
/api/*request, compared in constant time. The token lives in the launch URL and aSameSite=Strict,HttpOnlycookie. - No raw-SQL surface. No endpoint accepts SQL text. Builder and write requests are ORM args, validated identifier-by-identifier against the introspected schema; all values are
$Nparameters. BEGIN READ ONLYon every read. Even if every check above were bypassed, Postgres itself rejects writes on the read path. Without--write, this covers every request Studio can make.- Write-mode gates. With
--write: full-primary-key addressing on every mutation, per-request transactions with the same timeout and schema pinning, andOriginchecks. - Statement timeout. A 30-second transaction-local
statement_timeout(parameterizedset_config, never interpolated) bounds runaway queries;search_pathis pinned to the configured schema the same way. - Request hygiene. Per-session rate limiting (100 requests / 60 s), cross-origin requests refused, and security headers on every response: a nonce-based CSP (
script-src 'self' 'nonce-...'),X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer.
Deliberately not implemented, in any mode: raw SQL, DDL, and predicate-based mutations (UPDATE ... WHERE over arbitrary conditions). Every mutation names its rows by primary key. Studio is for inspection and the occasional targeted fix; use the CLI, migrations, or your own code for everything else.
See also#
- CLI, every command, flag, and the config resolution order.
- API Reference, the
findManysurface the builder compiles to, and theincludePiiread option (unlocked by theUNSAFEsymbol, never bytrue). - Observability,
npx turbine observe, the metrics dashboard with the same security model.