Studio

fenec studio is the server's own admin page: its collections, their rows and their schema in a browser, the edits you make there, a query editor, the rows as they change and the server's own numbers. Start the server with --studio and open /_studio/, or try it on the Playground, where it runs over a database in your own tab.

Turn it on

Add --studio to fenec-server, open /_studio/ on the same address, and paste a token.

fenec-server --file data.fenec --http 127.0.0.1:8080 \
         --http-token "$TOKEN" --studio

open http://127.0.0.1:8080/_studio/
fenec studio: the collections on the left with their row counts and a collection's fields and indexes, 100 000 orders in a grid with counts by status and owner above it, and one row unfolded on the right, its json field as a tree and its vector summarised
100 000 orders, counted by their @hash fields, one row unfolded.
  • Off by default. Without --studio, /_studio/ answers 404, as an unknown path does.
  • No new authority. The page reads and writes with the token you paste, over the same HTTP surface every client uses.
  • A tool for operators: keep it on a private network or behind your own sign-in (Security).
Details

The studio is plain ES modules and CSS from the repository's studio/ folder, with the HTTP client and query builder of @fenecdb/web/client beside them — no framework and no build step. The server embeds the files, so the binary is all you deploy. Its first load is 64 KB of JavaScript and CSS, gzipped, its modules preloaded together from the page; the query editor, the schema, the live rows and the admin view are fetched the first time each is opened, 7 to 19 KB more. The server sends the files as they are, uncompressed; a proxy in front may compress them.

A router serves it too: fenec-shard --studio puts the same pages on the router's address, where each tenant is reached through the router (Tenants).

Connect

The server is the page's own address unless you name another, and the token is the one the server takes: its --http-token, or a JSON Web Token its policy holds to its rows. The top bar says who the server takes it for.

  • Your token stays in the tab. It is kept in session storage and forgotten when the tab closes: never in local storage, a cookie or a URL.
  • Who you are: the token's subject, role and tenant, the rules that apply to it, and how long until its exp. The line under the top bar shortens as the token's life runs out, and the page warns you five minutes before it does.
Details

The identity comes from GET /_whoami, which any client may ask: open on a server that asks for no token, full for the server's token, and scoped for a JSON Web Token, with each rule of the policy that applies to it — the collection, the grants, the fields an update may change, the fields a read shows (read_fields, null for every one) and the rows, as the policy writes the filter. A token is shown its own rules and no other's: a rule for a role it does not hold is not among them. It grants nothing; a token the server refuses gets the same 401 here as anywhere.

curl -H "Authorization: Bearer $ALICE" http://127.0.0.1:8080/_whoami
# {"node":"single","tenant":null,"read_only":false,"kind":"scoped","sub":"alice",
#  "tenants":null,"unbound":false,"append_only":[],
#  "rules":[{"collection":"orders","grants":["read","insert","update","delete"],
#            "fields":null,"read_fields":null,"rows":"owner = $jwt.sub"}]}

The token's claims — its subject, role and exp — are read in the page to show them, and decide nothing: the server checks the token on every request.

Collections and rows

The sidebar lists the collections the token can read, each with its rows and its fields' types and indexes. The grid shows a collection's rows, as many as it holds, read a page at a time as you scroll.

  • Sort by id, or by a field an index orders: @sorted or @ttl.
  • Filter with a where clause in FenecQL and its parameters, or a box under each column: abc, =v, >=v, a..b, null.
  • Counts by value for each @hash field, over what the filter selects; pick one to filter by it.
  • A row unfolded (Space): a json field as a tree, a vector as its dimension, its length and its first components.
  • File size and what a compact would give back, where the token may read /_metrics.
Details

A page is 100 rows. In id order the page after one already read is asked for by its last id — get orders where id > $1 limit 100 — which walks the id index from there; a page reached by a jump, or under an order, by its offset. Only the rows in view are drawn, so 100 000 rows cost what 40 do: a scroll down the whole collection and back in Chrome took no task over 50 ms (make studio-test). Past 8 million pixels of rows the scroll bar stands for the rows in proportion, since browsers cap an element's height.

The count and the counts by value are one /batch of reads, under one read lock, so they are of one moment. A vector is never written out whole: 768 numbers in a cell are noise.

Edit

Enter writes a cell, N opens a form for a new row, and Delete removes one after you confirm the statement it will send.

set orders {note: $1} where id = $2 require 1
insert orders {customer: $1, total: $2}
del orders where id = $1 require 1
  • Every value is a parameter. A text shaped as FenecQL is stored as it reads.
  • Every write is a /batch with an Idempotency-Key, so a retry cannot write it twice.
  • A refusal says why: 403 names the field the policy refused, and 412 says the row changed — deleted, or no longer the token's — since it was read.
Details

require 1 makes a write that found no row a refusal (412) rather than a quiet affected 0, so an edit of a row someone deleted meanwhile is told so. The form is built from the schema: a box a field, typed as the field is, the statement written out below it as you fill it. A name FenecQL cannot write is refused rather than spliced into the text, and the where clause you type is held inside parentheses it cannot close, so it stays one condition among the page's.

Query

The Query tab is an editor for FenecQL. It runs exactly what you type, with your token: it can do what the token may, and nothing more.

fenec studio's query editor, dark: a get over orders with two parameters, coloured as the docs colour FenecQL, its 200 rows in the grid under it with counts by owner, the status, the time, the rows and the request id beside the tabs, and the saved and past queries on the right
A query with its parameters, its rows, and the request id to find it in the logs.
  • Run with Ctrl+Enter or ⌘Enter; a selection runs alone.
  • Parameters are a JSON list, bound to $1, $2…
  • Several statements, separated by ;, run as one /batch: they land together or not at all. Give their parameters as a list each, [[1], ["a"]].
  • The answer as rows or as JSON, with its status, its time, its Fenec-Seq for a write and its X-Request-Id.
  • The plan of a get: the Plan tab asks the server for explain of the same text with the same parameters.
  • A refusal shows the server's message and status, and for a batch which statement stopped it, which the editor selects.
  • History and saved queries stay in this browser. The token is never kept there.
Details

The colours are the docs' own rules: site/build.py writes the same patterns and keywords into the editor's highlighter, and site/test_highlight.py holds the two to the same output. The text is drawn over the textarea as text nodes, so the caret, selection, input methods and undo are the browser's. Nothing you type is rewritten: the editor only cuts the text at each ; outside strings and comments, and a statement goes to POST /query, several to POST /batch, as any client sends them.

Schema

The Schema tab shows a collection as the statements that make it, and its fields. With the server's token you can add an index or a field, rename or drop a field, and drop the collection.

fenec studio's schema view, light: adding a sorted index on the total field of orders, the dialog showing the create index statement it will run and the same statement from the server's declared-schema plan
An index on its way: the statement, and what the server's plan says of it.
  • Every change is shown before it runs: the exact statement, and what POST /_schema/plan says of the schema as it would be after it.
  • A drop waits for its name. Dropping a field or a collection runs only once you type its name.
  • A scoped token reads the schema of the collections it reads, and changes nothing.
create index on orders (total) @sorted
alter collection orders add field due timestamp @ttl(30d)
alter collection orders rename field note to memo
alter collection orders drop field memo
drop collection scratch
Details

The statements come from the server: GET /_schema?as=fenecql answers the description with its collections as FenecQL, written by the engine. The plan is asked of the collection as it would be after the change, and of no other, so it speaks of this change alone. It writes the statements an index or a field added would need, and refuses to guess a field that is gone: a drop or a rename? The statement says which. A change runs as a /batch under an Idempotency-Key, as the studio's other writes do. A name FenecQL cannot write is refused, and an index's options and a ttl's duration are checked before they reach the text.

Live

The Live tab follows a collection's rows as they change: new and rewritten rows are marked as they arrive, deleted ones struck through before they go.

fenec studio's live view, light: the orders of one customer, a new row and a rewritten one marked at the top, and the changes listed on the right with their change numbers
One customer's orders as they are written, each change listed.
  • A where clause picks the rows to follow.
  • Pause holds the changes back and counts them; resume applies them in order.
  • A scoped token sees its own rows: the server holds the stream to them, deletions included.
Details

The view is a subscription, GET /<name>/changes with your filter as its shape: first every row it selects, then each write. So a shape of more than 10 000 rows is refused before it opens: narrow it with the where clause. Under a scoped token the server ANDs the token's filter into the shape and keeps the ids it sent, so a row the token may not read never reaches the page, nor does the deletion of one.

Admin

The Admin tab is shown to the server's own token. It reads the statements by shape, the server's counts and, on a router, its tenants.

fenec studio's admin view, dark: statement shapes ordered by their mean time with their calls, maximum, rows and errors, and the server's reads, writes, latency, file size and reclaimable bytes beside them
The slowest shapes, and the server's numbers.
  • Statements: the slowest and the most run shapes, from /_stats/statements. Pick one to open it in the editor.
  • Server: reads, writes, errors and their latency, the file and what a compact would give back, memory and compactions, from /_metrics, read again every 5 seconds.
  • On a router: each tenant with its node, state and size on disk, from GET /_shard/tenants?bytes.
Details

The tab appears when /_whoami says the token is the server's, or the server asks for none; the server decides again on every request. Through a router the tenants' list takes the router's --token: start the router with the nodes' data token and one token reads both.

In your browser

The site's Playground is this studio — the same files the server embeds — over a database in your browser tab rather than a server's: fenecdb compiled to WebAssembly, on a worker of the tab, seeded with products, orders and a week of events. Nothing is sent anywhere. Rows, the query editor, the schema and the live rows work there as they do here; the query editor lists examples to run.

On a serverIn your browser
A token pasted in, its subject and expiry in the top barRunning in your browser. in its place, and nothing to sign in to
The tenant picker on a node or a routernone: one database
The admin view, the file's sizes in the sidebarnone: those are a server's numbers
Writes through /batch with an Idempotency-Keythe same statements, one block each batch
Live rows through a subscriptiona live query over the module's change ring
Kept in the server's filekept nowhere unless asked: Keep it in this browser stores it in IndexedDB, and Reset data puts every collection back
Details

Every view reads through one surface: run, batch and subscribe as FenecHttp has them, and request for an answer shown whole — the editor's status and change, the schema and its plan. Over a server that surface is HTTP (studio/connect.js); in the page it is the browser module in a dedicated worker (studio/local.js, worker.js), which answers each request as a server would: the same rows, the status a server refuses a statement with, the change a write leaves the database at, the schema as the engine describes it (Fenec.describe) and a batch as one block (Fenec.batch). studio/test/transport.test.mjs sends the same statements to a real fenec-server and to the module and holds every answer equal. A server's first load holds none of it: the page's own modules load only where a page asks for them.

Keys

/the where clause
Alt+1, Alt+2the collections, the view
arrows, Page Up, Page Downmove between cells
Enter, F2edit the cell
Spacethe whole row
Ccopy the row as JSON
Ssort by the column, where its index orders it
N, Deletea new row; delete the row, once confirmed
R, ?read the rows again; every key
1 to 5rows, query, schema, live, admin (1 to 4 in your browser)
Ctrl+Enter, ⌘Enterrun the query, in the editor

Tenants

On a tenant node or a router, the top bar picks the tenant: those the token's tenant claim names, or the node's or router's list where the token may read it. Otherwise type the tenant when you connect.

fenec-server --dir tenants --http 127.0.0.1:8081 --admin-token "$ADMIN" --studio
fenec-shard --listen 127.0.0.1:8090 --directory shard.fenec --studio
Details

A token is bound to the tenants it names on a node, so the picker offers nothing it would be refused. Through a router the studio is on the router's address and every tenant is that same origin; a page served by a node would be another origin to the router. --studio-connect <origin> lets the pages a server serves send requests to one more origin, which its content security policy names.

fenec studio on a phone, light: the orders grid with its where clause above fenec studio on a phone, dark: the same grid
On a phone, light and dark: the collections slide in from the side.

Security

The studio is a new surface, not a new authority. Treat it as you would the server's own port: keep it on a private network, or behind your own sign-in and TLS.

authoritynone of its own: every statement carries the pasted token, so a scoped token sees its rows alone — in the grid, the counts and the facets
the tokensession storage of one tab; sent only as Authorization: Bearer
headersContent-Security-Policy with default-src 'self', no inline script, connect-src 'self' and the --studio-connect origin; X-Frame-Options: DENY; nosniff; no referrer
valuesshown as text, never as markup; sent as parameters, never spliced
the editorruns what you type, as typed, with your token: no more authority than the token
schema changesshown as their statement and the server's plan before they run; a drop runs once its name is typed
this browserkeeps the editor's history and saved queries, and the theme; never the token
cachingthe page is never kept; its modules are asked about again each load (ETag, 304)
offwithout --studio, 404