Skip to content
DocspackagesDocumentation

@lunora/studio

The Lunora Studio — a local admin console for your schema, data, functions, logs, and advisors.

PackagesStudio

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

DomainPages
OverviewHome · Dashboards
DatabaseData · SQL editor · Schema · Migrations · Vectors · Time Travel · Export / Import
FunctionsFunctions · API · Workflows · Queues · Containers
AuthUsers · Organizations · Sessions · Configuration
StorageFiles · Access Rules · KV
ObservabilityIssues · Logs · Traces · Audit · Realtime · Fan-out · Containers · Metrics · Analytics · Health · Deployment health
AdvisorsSecurity · RLS Policies · Permissions · Performance
OperationsScheduled · Mail · Log drains · Payments · Flags
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).

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 dataEditable is 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.
  • 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 runAsIdentity enabled, 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.trace waterfalls 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/health endpoint.

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 the lunora/notify.ts config'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 (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.