@lunora/studio
The Lunora Studio — a local admin console for your schema, data, functions, logs, and advisors.
The Lunora Studio is a self-contained admin console for inspecting and operating a Lunora backend: browse and edit data, run functions, read logs, inspect the schema, run advisors, manage auth, and more. It ships as a React component library so you can mount the whole console, embed a single panel, or compose your own layout.
It is modelled on Supabase Studio's two-zone console: a slim icon rail of domains down the left, a secondary nav listing that domain's pages, and the active panel filling the rest. Every page is a real, shareable URL (TanStack Router over the History API), so deep links and back/forward work.
Running the studio
Three ways, smallest setup first:
lunora dev(zero config). The@lunora/viteplugin serves the studio at/__lunoraduring dev and prints the URL on startup. Add@lunora/studioto your project's deps and it just works; opt out withlunora({ studio: false }).- Standalone app.
apps/studiois a deployable Vite SPA that points at any worker viaVITE_LUNORA_URL— for hosting the studio separately from dev. - Embed. Mount the batteries-included console, or compose individual panels
under your own
<LunoraProvider>.
// mount the whole console into <div id="root">
import { mountStudio } from "@lunora/studio/mount";
mountStudio({ baseUrl: "https://my-app.workers.dev" });// or embed the shell / a single panel in your own React app
import { StudioApp, Studio, DataBrowser } from "@lunora/studio";
<StudioApp baseUrl="https://my-app.workers.dev" />;
<Studio dataEditable functions={LUNORA_FUNCTIONS} />;The <Studio> shell takes a few host-controlled switches worth knowing:
dataEditable— allow row insert/edit/delete (off by default; the console is read-only until the host opts in).runAsIdentity— let the function runner execute as a chosen authenticated identity, to test auth + RLS. Security-sensitive (it forges identity on an admin RPC) — only ever enable on a trusted loopback-dev gate, never in production.functions— the descriptors that populate the function runner and API tab.initialShardKey— the shard every shard-scoped panel targets on first load.
How the console is laid out
The icon rail has nine domains, top to bottom, with Settings pinned to the bottom. Each owns a set of pages:
| Domain | Pages |
|---|---|
| Home | Home |
| Database | Data · SQL editor · Schema · Migrations · Vectors · Export / Import · Time Travel |
| Functions | Functions · API · Workflows |
| Auth | Users · Organizations · Sessions · Configuration |
| Storage | Files · Access Rules |
| Reports | Dashboards · Metrics · Health |
| Advisors | Security · RLS Policies · Performance |
| Logs | Logs · Audit · Scheduled · Realtime · Mail · Log drains · Payments |
| Settings | Settings |
Most shard-scoped pages share a shard-key input — Durable Objects aren't enumerable server-side, so you target a shard by key. The data and SQL pages remember the shards you've visited and offer a "Shards seen" picker with a live table/row-count summary. A ⌘K command palette jumps to any page by name.
The rail also collapses to just icons, and the whole console localises its own
UI strings (locale / i18n props).
Home
The landing overview: connection state, deployment health, and a roll-up of advisor findings (security, performance, schema) so the first thing you see is whatever needs attention. Drill into any summary to its panel.
Database
The data console — everything that reads or shapes stored state.
- Data — Browse and edit rows across your shard and global tables. A
full-height table editor: pick a table, page through rows (table or JSON view),
filter, and — when
dataEditableis on — insert, patch, and delete. It covers both shard tables and.global()(D1-backed) tables. Two power tools:- Mask preview — a "Mask sensitive columns" toggle that previews
redact/hash/custom output client-side and flags masked columns in the grid
header, mirroring what data masking does on the
server. Codegen emits the
(table, column, strategy)map it reads. - Row generation — generate realistic seed rows with
@faker-js/faker, inferred from each column's type, before inserting. - Cascade preview — before deleting a row, see which related rows a cascading relation would take with it.
- Mask preview — a "Mask sensitive columns" toggle that previews
redact/hash/custom output client-side and flags masked columns in the grid
header, mirroring what data masking does on the
server. Codegen emits the
- SQL editor — Run read-only SQL against a shard. A full-height query
console for ad-hoc
SELECTs against a shard's SQLite. - Schema — Inspect each table and its columns. Every table with its row count, expandable to its columns, indexes, and relations. It also renders an interactive schema diagram (React Flow): tables as nodes, relations as edges, with a storage-tier filter (shard / global), a find box, and export to PNG / SVG / JSON. The Insights "add the index" deep-link lands here with the offending table pre-expanded.
- Migrations — Review migration status and run them. Inspect data-migration run-state and kick one off.
- Vectors — Browse Vectorize indexes and run similarity searches.
- Export / Import — Export a shard to NDJSON, or import rows from it.
- Time Travel — Restore a shard to a point in the last 30 days (PITR).
Functions
- Functions — Run registered queries, mutations, and actions. Pick a
function, edit its JSON args, and invoke it; per-function call/error stats sit
above the runner. With
runAsIdentityenabled, run a function as a chosen identity to test auth and RLS behaviour. - API — Interactive OpenAPI reference and copy-paste snippets for your functions. Renders the generated OpenAPI 3.1 / OpenRPC documents.
- Workflows — Inspect declared Cloudflare Workflows and their bindings. Lists every durable workflow with its export name, generated class, binding, and deployed name; starts an instance from a JSON-params form; and tracks instances with their live status (queued / running / complete / errored) and output.
Auth
Backed by @lunora/auth's admin API. Capability-driven —
each page shows only when its better-auth plugin is enabled.
- Users — Manage auth users — roles, bans, sessions, and identity.
- Organizations — Browse and manage organizations, members, and invitations.
- Sessions — Browse and revoke active sessions across all users.
- Configuration — Enabled plugins and session config (read-only).
Storage
- Files — Browse objects in your R2 storage buckets by prefix.
- Access Rules — Inspect storage access rules — per bucket, operation, and key prefix.
Reports
- Dashboards — Chart widgets backed by saved read-only SQL queries.
- Metrics — Per-shard health and aggregate metrics — request/error counts, uptime, DB size, reactive-cache hit rate.
- Health — At-a-glance connection, error, and shard signals.
Advisors
The advisors surface — splinter-style lints over your schema, functions, and runtime signal.
- Security — Review admin gates, credentials, and log redaction — the security-category findings.
- RLS Policies — Inspect row-level-security policies and roles, per table.
- Performance — Surface slow functions, error spikes, and cache problems. Runtime insights derived from each shard's durable metrics; findings deep-link to the fix (e.g. the Schema page to add a missing index).
Logs
Operational streams and the optional package-backed pages.
- Logs — A live stream of recent function logs (the shard's log ring buffer).
- Audit — A durable log of admin state-changing operations.
- Scheduled — Inspect and cancel scheduled jobs (
@lunora/scheduler). - Realtime — Active WebSocket subscriptions on this shard.
- Mail — Email your app sent, captured in dev (
@lunora/mail). - Log drains — Forward logs to Logpush, Tail Workers, or a webhook collector.
- Payments — Synced customers, subscriptions, and webhook events.
Settings
Read-only deployment config — vars, secrets, and bindings. Pinned to the bottom of the rail.
Admin gate
The studio reaches the backend two ways: reserved __lunora_admin__:* RPCs that
ShardDO intercepts (data, schema, metrics, logs, migrations, export), and
admin-gated worker endpoints under /_lunora/admin/* for things that live
outside the shard (scheduler, storage, functions, global tables, auth).
Both are disabled unless the server sets LUNORA_ADMIN_TOKEN, and the client
must present a matching Authorization: Bearer token. The components issue no
credentials of their own — configure the client's auth token at the host.
// worker entry — every admin surface is gated by adminToken
createWorker({
shardDO: env.SHARD,
adminToken: env.LUNORA_ADMIN_TOKEN,
schedulerDO: env.SCHEDULER, // enables the scheduled-jobs page
functions: LUNORA_FUNCTIONS, // enables function discovery
storageList: createStorage({ bucket: env.FILES }).list, // enables the file browser
});globalIntrospector (D1 tables) and authAdmin (the user-management +
organization dashboard, via @lunora/auth's createAuthAdmin(auth)) wire up the
remaining pages; see the package README for the full per-page configuration. The
auth dashboard is capability-driven — it shows only the surfaces whose
better-auth plugin is enabled. Pages whose data source isn't configured are
omitted from the <Studio> shell.
Optional-package nav gating
The studio also hides the pages for optional @lunora/* packages your app
doesn't use — Payments, Mail, Files + Access Rules, Vectors, Scheduler, and
Workflows — so you never land on a page that errors with "unknown table". This is
automatic: codegen statically detects which features a deployment wires up and
emits the result into the generated ShardDO, which the studio reads once over
the __lunora_admin__:studioFeatures RPC.
A feature's page shows when any of these is true:
- a
lunora/source imports its package (e.g.@lunora/mail) or reads its context helper (ctx.payments,ctx.storage,ctx.vectors,ctx.scheduler,ctx.workflows); - a schema signal implies it — a
v.storage()column or storage access rule (Files), a declared cron (Scheduler), a vector index (Vectors), or a declared workflow (Workflows); - the package is a declared dependency in your
package.json— so a package wired only in your worker entry (outsidelunora/), like@lunora/mail, still shows its page.
The gating fails open: every page stays visible until the RPC resolves, and a
worker predating the RPC (or one that errors) keeps showing everything. A page is
only ever hidden once the worker positively reports its feature as unused — so the
worst case is an extra empty page, never a missing working one. No configuration
is required; re-run codegen (lunora dev does this on save) after adding or
removing a package and the nav updates itself.
See also
- @lunora/vite — the
studioplugin option - @lunora/cli —
lunora dev - Advisors — the lints behind the Advisors domain
- Data masking — the mask preview in the data browser
- Workflows — the Workflows inspector
- Architecture