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; 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 whole 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 |
|---|---|
| Overview | Home · Dashboards |
| Database | Data · SQL editor · Schema · Migrations · Vectors · Time Travel · Export / Import |
| Functions | Functions · API · Workflows · Queues · Containers |
| Auth | Users · Organizations · Sessions · Configuration |
| Storage | Files · Access Rules · KV |
| Observability | Issues · Logs · Traces · Audit · Realtime · Fan-out · Containers · Metrics · Analytics · Health · Deployment health |
| Advisors | Security · RLS Policies · Permissions · Performance |
| Operations | Scheduled · Mail · Log drains · Payments · Flags |
| 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).
Overview
- Home: Connection, health, and advisor summary for your deployment. 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.
- Dashboards: Chart widgets backed by saved read-only SQL queries.
Database
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. Three extra 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.
- Time Travel: Restore a shard to a point in the last 30 days (PITR).
- Export / Import: Export a shard to NDJSON, or import rows from it.
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.
- Queues: Inspect declared Cloudflare Queues: their producer bindings,
consumer mode, and dead-letter queue (
@lunora/queue). - Containers: Live Cloudflare Containers: current lifecycle state per
container from the log stream (
@lunora/container).
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.
- KV: Browse and edit key-value pairs in your Workers KV namespaces
(
@lunora/bindings).
Observability
Operational streams and per-shard signal from a running deployment.
- Issues: Grouped error triage: Worker throws and container crashes folded
by fingerprint (
@lunora/fingerprint), so many instances of one bug read as a single row. - Logs: A live stream of recent function logs (the shard's log ring buffer).
- Traces: Recent
ctx.tracewaterfalls for this shard, the drill-down from a log line (see observability). - Audit: A durable log of admin state-changing operations.
- Realtime: Active WebSocket subscriptions on this shard.
- Fan-out: Realtime fan-out cost and per-topic subscriber counts for this shard.
- Containers: Live Cloudflare Containers: current lifecycle state per
container from the log stream (
@lunora/container). - Metrics: Per-shard health and aggregate metrics (request/error counts, uptime, DB size, reactive-cache hit rate).
- Analytics: Usage and latency from Analytics Engine (request volume, p50/p95, and hot shards).
- Health: At-a-glance connection, error, and shard signals.
- Deployment health: Live liveness, readiness, and per-binding health from
the deployment's
/_lunora/healthendpoint.
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.
- Permissions: Inspect access policies per table, and probe a function as any identity.
- 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).
Operations
The optional package-backed operational pages: scheduling, email, log forwarding, payments, and feature flags.
- Scheduled: Inspect and cancel scheduled jobs (
@lunora/scheduler). - 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.
- Flags: Inspect feature flags and their live evaluation under a
targeting context (
@lunora/flags).
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, Notifications, Files + Access Rules, Vectors,
Scheduler, and Workflows), so you never land on a page for a package you never
installed, which would error with "unknown table" or render a shell with nothing
behind it. Gating is about unused packages, not about emptiness: a page for a
package you do use still shows its own empty state when there is genuinely
nothing to list yet (Notifications with no registered devices, say). 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 thelunora/notify.tsconfig's@lunora/notify) or reads its context helper (ctx.payments,ctx.storage,ctx.vectors,ctx.scheduler,ctx.notify,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.