PackagesStudio

@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:

  1. lunora dev (zero config). The @lunora/vite plugin serves the studio at /__lunora during dev and prints the URL on startup. Add @lunora/studio to your project's deps and it just works; opt out with lunora({ studio: false }).
  2. Standalone app. apps/studio is a deployable Vite SPA that points at any worker via VITE_LUNORA_URL — for hosting the studio separately from dev.
  3. 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:

DomainPages
HomeHome
DatabaseData · SQL editor · Schema · Migrations · Vectors · Export / Import · Time Travel
FunctionsFunctions · API · Workflows
AuthUsers · Organizations · Sessions · Configuration
StorageFiles · Access Rules
ReportsDashboards · Metrics · Health
AdvisorsSecurity · RLS Policies · Performance
LogsLogs · Audit · Scheduled · Realtime · Mail · Log drains · Payments
SettingsSettings

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.

  • DataBrowse 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 dataEditable is 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.
  • SQL editorRun read-only SQL against a shard. A full-height query console for ad-hoc SELECTs against a shard's SQLite.
  • SchemaInspect 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.
  • MigrationsReview migration status and run them. Inspect data-migration run-state and kick one off.
  • VectorsBrowse Vectorize indexes and run similarity searches.
  • Export / ImportExport a shard to NDJSON, or import rows from it.
  • Time TravelRestore a shard to a point in the last 30 days (PITR).

Functions

  • FunctionsRun 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 runAsIdentity enabled, run a function as a chosen identity to test auth and RLS behaviour.
  • APIInteractive OpenAPI reference and copy-paste snippets for your functions. Renders the generated OpenAPI 3.1 / OpenRPC documents.
  • WorkflowsInspect 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.

  • UsersManage auth users — roles, bans, sessions, and identity.
  • OrganizationsBrowse and manage organizations, members, and invitations.
  • SessionsBrowse and revoke active sessions across all users.
  • ConfigurationEnabled plugins and session config (read-only).

Storage

  • FilesBrowse objects in your R2 storage buckets by prefix.
  • Access RulesInspect storage access rules — per bucket, operation, and key prefix.

Reports

  • DashboardsChart widgets backed by saved read-only SQL queries.
  • MetricsPer-shard health and aggregate metrics — request/error counts, uptime, DB size, reactive-cache hit rate.
  • HealthAt-a-glance connection, error, and shard signals.

Advisors

The advisors surface — splinter-style lints over your schema, functions, and runtime signal.

  • SecurityReview admin gates, credentials, and log redaction — the security-category findings.
  • RLS PoliciesInspect row-level-security policies and roles, per table.
  • PerformanceSurface 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.

  • LogsA live stream of recent function logs (the shard's log ring buffer).
  • AuditA durable log of admin state-changing operations.
  • ScheduledInspect and cancel scheduled jobs (@lunora/scheduler).
  • RealtimeActive WebSocket subscriptions on this shard.
  • MailEmail your app sent, captured in dev (@lunora/mail).
  • Log drainsForward logs to Logpush, Tail Workers, or a webhook collector.
  • PaymentsSynced 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 (outside lunora/), 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