Help us shape the future of Go! Take the 2026 Go Developer Survey

orbit

package module
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

Orbit

The pluggable admin product for the Nucleus framework.

Go Reference Go

Orbit is a self-contained admin panel — Data Studio, a live request/SQL feed, a session viewer, RBAC management, and system metrics — that mounts in-process into any Nucleus application through the framework's extension/module API. It is a separate Go module with its own release cadence, extracted from the framework core per nucleus ADR-019 so the core stays lean and the admin can evolve as its own product.

You add one dependency and one Mount(...) call; orbit reads everything it needs from the running app's Runtime and serves its embedded React SPA — no separate asset deployment, no out-of-process sidecar, no database of its own.


Framework floor. Orbit builds against Nucleus and moves in lockstep with the certified suite set. The exact version it requires is in go.mod — that file is the source of truth, so this note cannot go stale.

Features

Module What it does
Data Studio Browse, create, edit, and delete records for every model in the app's registry — tenant-aware, with import/export.
Live runtime inspector Real-time feed of incoming HTTP requests and executed SQL across the whole app (sourced from the framework's observability event bus), with optional cross-node aggregation.
Session viewer List active server-side sessions — whose, from what device, which one is you — and revoke one or every session of an account.
Access control (RBAC) Inspect and manage the Casbin policies and roles backing the app's authorizer.
System metrics Runtime and resource consumption — CPU, memory, goroutines, database pool.
Audit log A durable trail of admin actions, with retention, export and per-record history.
Overview & Health Dashboard and health at a glance.

The UI ships embedded in the binary (go:embed), version-pinned to the orbit module — a consumer who mounts orbit gets the full admin offline, in a single binary.

Install

go get github.com/jcsvwinston/orbit@latest

The current tagged release is v1.16.0; pin that tag for reproducible builds.

v1.0 promise: the public surfaces (the root module and datasource) are frozen for the life of v1.x — enforced by contracts/freeze_test.go (see docs/V1_GATE.md).

Requires Go 1.26+ and a Nucleus application to mount into.

Quick start

Mount orbit on the application builder:

import (
    "os"

    "github.com/jcsvwinston/nucleus/pkg/nucleus"
    "github.com/jcsvwinston/orbit"
)

func main() {
    app, err := nucleus.New().
        FromConfigFile("nucleus.yml").
        Mount(orbit.Module(orbit.Config{
            Prefix:            "/admin",
            Title:             "Acme Admin",
            BootstrapUsername: "admin",
            BootstrapEmail:    "admin@acme.test",
            // When BootstrapPassword is empty, bootstrapping is skipped —
            // provision the admin user another way (e.g. nucleus createuser).
            BootstrapPassword: os.Getenv("ADMIN_BOOTSTRAP_PASSWORD"),
        })).
        Build()
    if err != nil {
        panic(err)
    }
    _ = nucleus.Run(app)
}

Start the app, open /admin, and sign in with the bootstrap user. Orbit self-registers its prefix with the framework's default-deny RBAC and enforces its own session-based auth below that prefix.

The zero value is valid too — orbit.Module(orbit.Config{}) mounts under /admin with sensible defaults.

Signing in against your directory

If the host application declares an authentication chain, the admin panel uses it:

auth_backends: [ldap, local]

Orbit ships no LDAP client. It asks the framework's chain, so whatever the operator configured for the application applies to the panel too.

Authentication is delegated; authorization is not. The chain answers who the credentials belong to; this panel's own admin table still decides whether that person may enter. A directory account that is not an administrator here is refused — otherwise connecting a corporate directory would quietly make every employee in the company an administrator of your admin panel.

That also means a local admin row is not a bypass: with a chain configured, the chain still has to accept the password, so a revoked directory account cannot get in through a stale row.

Without auth_backends, nothing changes: the panel validates against its own table exactly as before.

Configuration

orbit.Config is bound from the modules.orbit.* subtree of your nucleus.yml (or set it directly in Go). All fields are optional.

Precedence: the YAML subtree is overlaid on whatever you passed to orbit.Module(...), key by key — so Go supplies the base and nucleus.yml wins for the keys it sets. The one exception is prefix: the framework mounts the module before it binds the subtree, so modules.orbit.prefix cannot move the panel. Setting it to something other than the mount point fails at startup, naming both values, rather than serving a panel whose own links point elsewhere.

Key (modules.orbit.*) Type Default Description
prefix string /admin URL path orbit mounts under. Must agree with the prefix passed to orbit.Module(...); see the precedence note above.
title string — Heading shown in the UI.
environment string — Label shown in the UI (e.g. production).
bootstrap_username string — Admin user created on first boot.
bootstrap_email string — Email for the bootstrap user.
bootstrap_password string — Password for the bootstrap user; empty → bootstrapping is skipped (provision the admin user another way, e.g. nucleus createuser).
auth_database string app default DB alias whose handle backs admin auth + the bootstrap user (use a dedicated DB for the admin user store).
migrations_path string migrations Directory the migrations view reads.
audit_max_size int 10000 In-memory audit-log ring size.
multitenant_enabled bool false Confine Data Studio (list, get, create, update, delete, bulk, CSV export, exports and their jobs, imports, fixtures) to the tenant the host app resolves for the request; a request with no resolved tenant and no default is a 403. ?tenant=<id> / ?tenant=all are accepted only from a superuser or a subject granted the tenant_switch RBAC action, and audited as tenant.override. The host's resolution must not be client-controlled (a header the client sets) for the confinement to hold.
multitenant_default string — Tenant applied when the host resolves none; without it such a request is refused unless the operator may switch tenants.
multitenant_ids []string — Known tenant IDs for the selector UI.
live_exclude_patterns []string — Path patterns excluded from the live HTTP feed.
trace_url_template string — External trace-explorer URL template (supports {trace_id}).
cluster_enabled bool false Aggregate the live feed across nodes via a Redis relay.
cluster_redis_url string — Redis URL for the live-telemetry relay.
cluster_channel string nucleus:admin:live:v1 Pub/sub channel for the relay.
cluster_node_id string runtime id Explicit node identifier in the relay.
cluster_token string — Shared secret to reject untrusted relay messages.
# nucleus.yml
modules:
  orbit:
    prefix: /admin
    title: Acme Admin
    environment: production
    bootstrap_username: admin
    bootstrap_email: admin@acme.test

How it works

Orbit is a nucleus.ModuleSpec. On startup it captures the app's Runtime and builds its panel from the framework's public accessors — the model registry, all managed database handles, the session manager, the RBAC enforcer, the live event bus, and storage. It never reaches into framework internals:

  • In-process — it sees live runtime state (sessions, SQL, model registry, metrics) that an out-of-process sidecar could not, without any IPC surface.
  • Self-contained auth — it owns a session-based login (DatabaseAdminAuth) against the nucleus_admin_users table and self-registers its prefix with the framework's default-deny RBAC, so the framework middleware never double-gates it.
  • Embedded SPA — the React UI is built into the module and served under the mount prefix.

Cluster observability (optional)

For fleet-wide live telemetry, orbit ships sibling modules that are releasable independently:

Module Role
orbit/proto Connect-RPC contract + generated stubs.
orbit/agent In-process agent (agent.NewExtension) that ships events to an admin server.
orbit/server Standalone admin-server binary that receives them.

Most applications only need the root orbit module for the in-process panel.

Relationship to Nucleus

Orbit is a "dogfooding" consumer of Nucleus: mounting a real, deep admin exercises and hardens the framework's extension/Runtime surface. The admin used to live in the framework core as pkg/admin; ADR-019 extracted it here as a clean break. Nucleus itself no longer ships any admin code.

Requirements

  • Go 1.26+
  • A Nucleus application to mount into.
  • (Optional) Redis — only for the cross-node live-telemetry relay.

Development

go work sync          # Go workspace: ./ ./agent ./proto ./quarkbridge ./quarkdatasource ./server
make build            # go build ./... in every module
make test             # go test ./... in every module

That is the quick inner loop. CI additionally builds and tests each of the six modules standalone (GOWORK=off, the way a consumer's toolchain resolves them), verifies the protobuf stubs and the two web UIs, and runs the repository guards. CONTRIBUTING.md walks through all of it — read it before opening a PR. Security reports go through SECURITY.md, never a public issue.

Documentation

Overview

Package orbit is the pluggable admin product for the Nucleus framework.

Orbit is a separate Go module that mounts in-process into a Nucleus application via the framework's extension/module API, and serves a self-contained admin UI (Data Studio, live request/SQL feed, session viewer, RBAC, system metrics). It was extracted from the framework core per nucleus ADR-019 so it can ship, version, and evolve as its own product while the core stays lean. Mount it explicitly:

app, err := nucleus.New().
    FromConfigFile("nucleus.yml").
    Mount(orbit.Module(orbit.Config{Prefix: "/admin"})).
    Build()

Orbit reads everything it needs from the nucleus Runtime — the model registry, the managed database handles, the session manager, the RBAC enforcer, the live event bus, storage (the accessors added in nucleus ADR-019 Slice 1/2) — so it never reaches into the framework's internals.

Index

Constants

View Source
const DefaultPrefix = "/admin"

DefaultPrefix is the URL path orbit mounts under when Config.Prefix is empty.

Variables

This section is empty.

Functions

func Module

func Module(cfg Config) nucleus.ModuleSpec

Module returns orbit as a nucleus ModuleSpec, mountable on an application via the builder's Mount(...). It is self-contained: it declares its own URL prefix and acquires every framework service it needs from the Runtime in OnStart, then mounts the admin panel's own router under the prefix in Routes.

Types

type ActionRequest added in v1.10.0

type ActionRequest = admin.ActionRequest

ActionRequest is what a ModelAction is told about the call: the model, the selected ids, the operator, the tenant in force and the database alias the grid was reading.

type ActionResult added in v1.10.0

type ActionResult = admin.ActionResult

ActionResult is what a ModelAction reports back — a message for the operator, how many rows it changed, and anything else the screen should get. Every field is optional.

type Branding added in v1.10.0

type Branding = admin.Branding

Branding is the logo, favicon and accent colour an application gives the panel (CUST-02). Every field is optional.

type Cache added in v1.10.0

type Cache = admin.Cache

Cache is what an application implements to give the panel a cache it can inspect and empty (OR-49): a name for the view, a count of entries, and a flush. It is an ALIAS of the panel's own interface, so an implementation satisfies both without the application naming an internal package.

The panel never reads or writes entries through it. Counting and emptying is the whole of what an operator does to a cache from a screen, and a contract that could also read entries would put cached values — session data, rendered fragments, whatever the application caches — behind a panel permission that was never meant to cover them.

type Config

type Config struct {
	// Prefix is the URL path orbit mounts under (default DefaultPrefix).
	Prefix string `yaml:"prefix" koanf:"prefix"`
	// Title is the heading shown in the admin UI.
	Title string `yaml:"title" koanf:"title"`

	// Bootstrap admin user, created on first start if it does not exist. When
	// BootstrapPassword is empty, bootstrapping is skipped (the operator is
	// expected to provision the admin user another way).
	BootstrapUsername string `yaml:"bootstrap_username" koanf:"bootstrap_username"`
	BootstrapEmail    string `yaml:"bootstrap_email" koanf:"bootstrap_email"`
	BootstrapPassword string `yaml:"bootstrap_password" koanf:"bootstrap_password"`

	// AuthDatabase optionally names a managed database alias whose handle backs
	// admin authentication and the bootstrap user. Empty means use the default
	// database. The panel itself (Data Studio etc.) always runs on the default
	// handle; only the auth/bootstrap *sql.DB is redirected.
	AuthDatabase string `yaml:"auth_database" koanf:"auth_database"`

	// Multi-tenant: set these to match the host application so Data Studio is
	// confined to the tenant the app resolves for each request (list, get,
	// create, update, delete, bulk, exports, imports, fixtures). Only a
	// superuser or a subject granted the tenant_switch RBAC action can look at
	// another tenant (?tenant=<id>) or at all of them (?tenant=all), and every
	// switch is audited. Leave disabled for single-tenant apps.
	MultiTenantEnabled bool     `yaml:"multitenant_enabled" koanf:"multitenant_enabled"`
	MultiTenantDefault string   `yaml:"multitenant_default" koanf:"multitenant_default"`
	MultiTenantIDs     []string `yaml:"multitenant_ids" koanf:"multitenant_ids"`

	// Row ownership: which column of a model says WHICH OPERATOR a row
	// belongs to, keyed by model name, with "*" as the default for every
	// model that carries the same column. It is what makes an RBAC policy on
	// admin:<Model>#own enforceable — the grant every editorial admin needs
	// ("an author edits their own posts"): lists are filtered by the column,
	// another operator's row is reported as not found, and a create stamps
	// it. A #own grant on a model that has no entry here is refused, never
	// widened to every row.
	//
	// RowOwnerSubject names which name of the operator the column holds:
	// "username" (default) or "id".
	RowOwnerFields  map[string]string `yaml:"row_owner_fields" koanf:"row_owner_fields"`
	RowOwnerSubject string            `yaml:"row_owner_subject" koanf:"row_owner_subject"`

	// FieldWidgets declares how a field is edited when its type cannot say
	// it: "Model.Field" (or "Model.column") to one of json, richtext, file
	// or image. A JSON document is inferred from the type and needs no
	// entry; "this string is HTML" and "this string is a storage key" are
	// claims about intent that no column type carries, so the application
	// makes them here. A file field gets an upload route
	// (POST /api/models/{model}/upload) that stores the bytes in the
	// application's own storage and answers with the key the form writes.
	FieldWidgets map[string]string `yaml:"field_widgets" koanf:"field_widgets"`

	// Environment is a label shown in the UI (e.g. "production"). Optional.
	Environment string `yaml:"environment" koanf:"environment"`
	// MigrationsPath is the directory the migrations view reads (default "migrations").
	MigrationsPath string `yaml:"migrations_path" koanf:"migrations_path"`
	// AuditMaxSize caps the in-memory audit log ring buffer; zero or negative
	// means the default of 10000 entries. It bounds the MEMORY store only: a
	// trail kept in the database is bounded by AuditRetentionDays.
	AuditMaxSize int `yaml:"audit_max_size" koanf:"audit_max_size"`

	// AuditStore selects where the audit trail is kept: "database" (the
	// default when the application has a database) writes a table the panel
	// owns and creates, so the trail survives restarts and is shared by every
	// replica; "memory" keeps the process-lifetime ring instead.
	AuditStore string `yaml:"audit_store" koanf:"audit_store"`

	// AuditRetentionDays drops trail entries older than that many days,
	// applied at mount and at most hourly afterwards. Zero keeps them until
	// somebody clears the log. It is a PERIOD, which is what a compliance
	// window is; AuditMaxSize is a count and answers a different question.
	AuditRetentionDays int `yaml:"audit_retention_days" koanf:"audit_retention_days"`

	// LiveExcludePatterns lists path patterns excluded from the live HTTP
	// capture feed (e.g. health checks, the admin's own polling endpoints).
	LiveExcludePatterns []string `yaml:"live_exclude_patterns" koanf:"live_exclude_patterns"`
	// ClusterEnabled turns on cluster-aware live telemetry: live request/SQL
	// events are relayed between nodes over Redis so the feed shows the whole
	// fleet, not just the local node. Best-effort — a relay failure never blocks
	// startup.
	ClusterEnabled bool `yaml:"cluster_enabled" koanf:"cluster_enabled"`
	// ClusterRedisURL is the Redis URL backing the live telemetry relay.
	ClusterRedisURL string `yaml:"cluster_redis_url" koanf:"cluster_redis_url"`
	// ClusterChannel is the Redis pub/sub channel the relay publishes on
	// (default nucleus:admin:live:v1).
	ClusterChannel string `yaml:"cluster_channel" koanf:"cluster_channel"`
	// ClusterNodeID is an explicit node identifier for this instance in the
	// relay (defaults to the runtime identity).
	ClusterNodeID string `yaml:"cluster_node_id" koanf:"cluster_node_id"`
	// ClusterToken is a shared secret the relay uses to reject untrusted
	// (cross-tenant or spoofed) messages on the channel.
	ClusterToken string `yaml:"cluster_token" koanf:"cluster_token"`
	// TraceURLTemplate is an external trace-explorer URL template surfaced in
	// the UI; it supports a {trace_id} placeholder.
	TraceURLTemplate string `yaml:"trace_url_template" koanf:"trace_url_template"`

	// Cache is the cache this application wants the panel to show and be
	// able to empty. Nil — the default — means the panel says there is no
	// cache here rather than offering a button that refuses.
	//
	// Nothing in the framework owns an application's cache (pkg/cache is a
	// library an application builds with, not a service the app wires), so
	// there is nothing for the panel to discover: the application is the
	// only thing that can say which cache the operator is looking at.
	// Go-only wiring; not bindable from YAML.
	Cache Cache `yaml:"-" koanf:"-"`

	// Actions are the verbs this application defines for its own models —
	// "publish these three drafts", "retry these payments" — which the
	// panel draws on the model's grid and runs under the same
	// authorization, tenant and row confinement as its own bulk verbs.
	// Nothing here can be discovered: the panel cannot know that a column
	// called status makes "publish" meaningful, so the application names
	// the verb and supplies the function. Go-only wiring; not bindable
	// from YAML. A declaration the panel cannot honour — an unknown model,
	// a duplicate verb, one of the panel's own — refuses to start.
	Actions []ModelAction `yaml:"-" koanf:"-"`

	// Pages are screens of this application's own, mounted inside the
	// panel: under its prefix, behind its session, gated by its RBAC and
	// listed in its navigation. The application writes the handler; the
	// panel supplies everything around it. Go-only wiring; not bindable
	// from YAML.
	Pages []Page `yaml:"-" koanf:"-"`

	// Branding is how the panel wears the application's clothes: the logo
	// on the sidebar and the login screen, the icon in the browser tab and
	// the accent colour. Empty fields keep Orbit's own. Unlike Actions and
	// Pages it is plain configuration, so it binds from nucleus.yml too.
	Branding Branding `yaml:"branding" koanf:"branding"`

	// Widgets are the cards this application puts on the panel's overview:
	// its own numbers, which the panel cannot discover. Go-only wiring;
	// each widget carries the function that reads its value.
	Widgets []Widget `yaml:"-" koanf:"-"`

	// Locale is the language the panel's own chrome opens in ("es",
	// "pt-BR"). It covers the panel's words — navigation, buttons, empty
	// states — and never the application's data: a model called Invoice is
	// called Invoice in every language. Empty means English.
	Locale string `yaml:"locale" koanf:"locale"`

	// Messages adds to or overrides the chrome's phrases, by locale and
	// key, including for a language the panel does not ship. A phrase
	// nobody translated reads in English rather than as its key.
	Messages map[string]map[string]string `yaml:"messages" koanf:"messages"`

	// DataSource overrides the source Data Studio browses and edits (ADR-001).
	// Nil means the default: a Nucleus-backed adapter over the application's
	// model registry and database handles. Set it to browse another backend —
	// e.g. an app that runs the Quark ORM passes a quarkdatasource adapter
	// (QADR-0006, Caso 2). Go-only wiring; not bindable from YAML. When set,
	// the runtime field-metadata editor is disabled (it mutates the Nucleus
	// registry, which a custom source does not necessarily have).
	DataSource datasource.DataSource `yaml:"-" koanf:"-"`
}

Config configures the orbit admin module. The zero value is valid (orbit mounts under DefaultPrefix); bound from the `modules.orbit.*` subtree of the application config when mounted on a config-file app.

The struct is flat, but its fields serve two different modes — do not let the cluster vocabulary scare a plain panel setup:

  • Panel (what almost every app uses): Prefix, Title, Bootstrap*, AuthDatabase, MultiTenant*, Environment, MigrationsPath, AuditMaxSize, LiveExcludePatterns, TraceURLTemplate. Nothing else is required: a prefix, a title and the bootstrap fields are a complete production shape.
  • Cluster live-feed relay (opt-in, off by default): the Cluster* fields. They only matter once ClusterEnabled is true; in particular, NO Redis is needed to run the panel — ClusterRedisURL is read exclusively by the relay. The standalone fleet plane (agent/, server/) is configured on its own binaries, not here.
  • Go-only: DataSource (not bindable from YAML).

Grouping the Cluster* fields into a nested sub-struct would make this split structural, but the flat yaml keys (cluster_enabled, ...) are part of the frozen surface below, so that reshuffle is deliberately deferred to a hypothetical v2; within v1.x the split lives in this comment and in the configuration docs.

Config is a frozen v1.0 surface (docs/V1_GATE.md §A-3): every field keeps its name, yaml key, type, and zero-value behavior for the life of v1.x. Fields may be added; none is removed or renamed without a major. The freeze is enforced by contracts/freeze_test.go.

type ModelAction added in v1.10.0

type ModelAction = admin.ModelAction

ModelAction is an action an application defines for one of its own models (DS-09): the verb on the wire and in the RBAC policy, the label on the button, and the function that runs it over the rows an operator selected. It is an ALIAS of the panel's own type, so an application declares one without naming an internal package.

The ids the function receives are already confined to what that operator may touch. That is the reason the action runs through the panel instead of being an endpoint of the application's own: it inherits the row and tenant policies the panel enforces, and the audit entry, rather than being the way around them.

type Operator added in v1.10.0

type Operator = admin.Operator

Operator is who is looking at an application Page.

func OperatorFromContext added in v1.10.0

func OperatorFromContext(ctx context.Context) (Operator, bool)

OperatorFromContext returns the panel operator a Page request was authenticated as.

type Page added in v1.10.0

type Page = admin.Page

Page is a screen of the application's own, mounted inside the panel (CUST-04). The handler is ordinary net/http; the panel authenticates the request, authorizes it against admin:page:<id>, puts the operator on the context (OperatorFromContext) and lists the page in the navigation.

It is a link, not a frame: the panel refuses to be framed (X-Frame-Options: DENY), and its Content-Security-Policy applies to the page too — so a page's scripts come from files, not from inline <script>.

type Widget added in v1.10.0

type Widget = admin.Widget

Widget is one card an application puts on the panel's overview (CUST-03): a title and the function that reads the value. The panel supplies the screen, the authorization (admin:dashboard), a timeout, and a card that says it could not be read rather than a screen that drops it.

type WidgetItem added in v1.10.0

type WidgetItem = admin.WidgetItem

WidgetItem is one row of a list widget.

type WidgetValue added in v1.10.0

type WidgetValue = admin.WidgetValue

WidgetValue is what a Widget shows: a headline value, a smaller detail line, or a short list of rows.

Directories

Path Synopsis
agent module
datasource module
internal
admin
Package admin provides an auto-generated administration panel for Nucleus, similar to Django's contrib.admin.
Package admin provides an auto-generated administration panel for Nucleus, similar to Django's contrib.admin.
proto module
quarkbridge module
server module
ui module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL