go-admin

module
v0.140.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT

README

Go Admin

go-admin is a feature-gated admin orchestration runtime for Go services. It wires modules, panels, commands, navigation, and route surfaces while letting hosts swap dependencies (auth, persistence, CMS container, activity, media, export, settings).

The repo provides two modules:

  • Core: github.com/goliatone/go-admin (dependency-light, framework-agnostic orchestration)
  • Quickstart: github.com/goliatone/go-admin/quickstart (opinionated defaults + adapters for faster bootstrap)

When to use core vs quickstart

  • Use core when you need strict control over dependencies, router/runtime wiring, and module registration.
  • Use quickstart when you want a working admin quickly with prewired defaults (feature gates, adapter hooks, Fiber/view helpers, theme/form/dashboard integrations) and then override selectively.

Runtime lifecycle (core mental model)

  1. Build admin.Config (title/base path, locales, URLs, permissions, feature-related config).
  2. Construct *admin.Admin with admin.New(cfg, deps).
  3. Optionally register custom modules before boot with adm.RegisterModule(...).
  4. Optionally register CMS bootstrap hooks with adm.AddCMSBootstrapHook(...) to create or repair host-defined content types before route snapshots are taken.
  5. Call adm.Initialize(router) once to run prepare + boot steps, reconcile dynamic CMS panels, and mount routes.

Initialize runs the boot pipeline (panels, dashboard, search, settings, jobs, notifications, translation routes, etc.), auto-registers built-in modules according to feature gates, and reconciles dynamic CMS panels before alias routes and any later quickstart content-entry UI binding snapshot panel state.

Quickstart follows the same lifecycle, but uses quickstart.NewAdminConfig(...) and quickstart.NewAdmin(...) to preload defaults and adapter wiring.

cfg := quickstart.NewAdminConfig("/admin", "Admin", "en")

adm, _, err := quickstart.NewAdmin(
	cfg,
	quickstart.AdapterHooks{},
	quickstart.WithMinimalFeatures(),
)
if err != nil {
	panic(err)
}

server := router.NewHTTPServer()
host := quickstart.NewHostRouter(server.Router(), cfg)
if err := adm.Initialize(host.Admin()); err != nil {
	panic(err)
}

When your host owns additional content types, register them through a bootstrap hook instead of manually reordering startup:

adm.AddCMSBootstrapHook(func(ctx context.Context, adm *admin.Admin) error {
	_, err := adm.ContentTypeService().CreateContentType(ctx, admin.CMSContentType{
		Name:         "Quote",
		Slug:         "quote",
		Status:       "active",
		Schema: map[string]any{
			"$schema":    "https://json-schema.org/draft/2020-12/schema",
			"type":       "object",
			"properties": map[string]any{"title": map[string]any{"type": "string"}},
		},
		Capabilities: map[string]any{"panel_slug": "quotes"},
	})
	return err
})

If you bind quickstart content-entry UI routes, do that after Initialize(...) so canonical routes such as /admin/quotes are derived from the reconciled registry snapshot.

Core-only wiring is the same shape, but you construct directly from admin.New:

cfg := admin.Config{BasePath: "/admin", Title: "Admin", DefaultLocale: "en"}
adm, err := admin.New(cfg, admin.Dependencies{})
if err != nil {
	panic(err)
}

if err := adm.RegisterModule(myModule); err != nil {
	panic(err)
}

server := router.NewHTTPServer()
if err := adm.Initialize(server.Router()); err != nil {
	panic(err)
}

Key extension points

  • Features: provide your gate implementation with core. With quickstart, use WithMinimalFeatures() or WithFeatureSet(...) to replace the base set and WithFeatureOverrides(...) to merge selected values.
  • Modules: register host modules with RegisterModule; mounted modules must expose RouteContractProvider so routing ownership stays explicit.
  • URLs: use Config.URLs + URLKit route names instead of hardcoding paths.
  • Scope policy: use Admin.EffectiveScope(...) or Admin.EffectiveScopeFromRequest(...) before scoped reads, writes, background syncs, and materialized read-model updates. In single-tenant mode, missing tenant/org values are filled from configured defaults; in multi-tenant mode they remain missing unless trusted actor, claims, record, or service input provides them. Authenticated admin APIs do not trust browser query tenant_id or org_id as actor scope.
  • Commands: register typed commands and message factories for panel/API dispatch.
  • RPC: expose selected commands through admin.commands.dispatch with deny-by-default command rules; see docs/GUIDE_RPC.md.
  • Panels: control canonical UI ownership (PanelUIRouteMode*) and canonical entry behavior (PanelEntryMode*).
  • Themes: wire admin go-theme selectors separately from public-site theme packages; see docs/GUIDE_THEME.md.
  • Auth and permissions: wire authentication, authorization, roles, scope, and diagnostics consistently; see docs/GUIDE_AUTH_PERMISSIONS.md.

Search Setup

go-admin exposes adapters for wiring go-search into public site search and admin global search, but host applications still own the go-search runtime, schema setup, migrations, and indexing.

See docs/GUIDE_SEARCH.md for admin global search, public site search, NewGoSearchBundle(...), route contracts, feature gates, permissions, and panel DataGrid search.

Media Responsibility Split

The upstream media surface is intentionally split across layers:

  • go-admin owns the admin-facing media module, gallery page, route-key-driven layout integration, and shared schema hints for admin pages.
  • go-formgen owns the field-level media-picker component, renderer templates, and picker runtime used inside forms.
  • Host apps keep ownership of storage providers, upload workflows, delivery URL rules, and any CMS-specific normalization around persisted media values.

That split keeps page-level media UX aligned with normal admin module/layout behavior while letting hosts keep app-specific delivery and persistence concerns out of core. See docs/GUIDE_FORMGEN.md for the broader form generation and component customization contract.

URL Configuration

Config.URLs is the canonical URL surface. It exposes separate namespaces for admin and public URLs, each with its own base path, API prefix, and version. Defaults preserve /admin for the admin UI and /api/v1 for the public API. If you need full control, set Config.URLs.URLKit to a custom URLKit config.

Example:

cfg := admin.Config{
	BasePath: "/admin", // legacy fallback + theme tokens
	URLs: admin.URLConfig{
		Admin: admin.URLNamespaceConfig{
			BasePath:   "/admin",
			APIPrefix:  "api",
			APIVersion: "",
		},
		Public: admin.URLNamespaceConfig{
			BasePath:   "",
			APIPrefix:  "api",
			APIVersion: "v1",
		},
	},
}

Routing Policy

admin/routing is the canonical routing policy package.

  • Hosts own absolute roots; modules declare relative route tables.
  • Mounted modules must implement admin.RouteContractProvider.
  • Startup routing validation is strict fail-fast in every environment.
  • Public API mounts are opt-in and only exist when a module declares PublicAPIRoutes.
  • Protected-app routing is also opt-in. Hosts must set Config.Routing.ProtectedAppEnabled=true before /app and /app/api roots are reserved.
  • When protected-app routing is enabled and no explicit roots are supplied, the canonical defaults are /app for the UI surface and /app/api for the protected API surface.
  • Protected-app root overrides are ignored unless protected-app routing is enabled.
  • Host runtime config may source these values from app.json or any other config system, but go-admin itself only consumes the normalized canonical fields on Config.Routing.
  • Quickstart supports both single-process and split-deployment composition with the same surface model: public-site deployments mount only the site surface, protected-app deployments mount ProtectedAppUI() and ProtectedAppAPI() with protected-app auth, and admin deployments keep the existing admin surfaces and auth wiring.

Operationally, the supported diagnostics are in-process:

  • adm.RoutingReport() for effective roots, resolved module mounts, warnings, and conflicts.
  • adm.RoutingPlanner().Manifest() for deterministic route-manifest export.

A stable HTTP JSON routing report endpoint is not part of the first release of the clean-break routing API. Use the in-process report/manifest APIs or quickstart doctor output instead.

See docs/GUIDE_ROUTING.md for the external-module contract, release policy, and CI manifest-review guidance.

CRUD, DataGrid, and Actions

Panel CRUD resources, DataGrid list wiring, and workflow/command-backed row and bulk actions are documented in docs/GUIDE_CRUD.md. Form generation, go-formgen integration, UI schema overlays, and custom form components are documented in docs/GUIDE_FORMGEN.md. RPC command transport and workflow RPC methods are documented in docs/GUIDE_RPC.md.

Panel Entry Modes

Panels can customize what the canonical panel URL (/admin/<panel>) renders.

  • PanelEntryModeList (default): render the panel list/datagrid view.
  • PanelEntryModeDetailCurrentUser: resolve the current authenticated user ID and render the panel detail view for that record.

Use PanelBuilder.WithEntryMode(...) in module/panel registration. This is complementary to PanelUIRouteMode:

  • PanelUIRouteModeCanonical: quickstart canonical panel UI routes are registered and honor EntryMode.
  • PanelUIRouteModeCustom: module-owned routes are responsible for entry behavior.

CMS CRUD Alignment (Read/Write Split)

Admin page CRUD now uses an explicit admin read model (AdminPageRecord) plus split read/write services. List and detail reads go through AdminPageReadService, while writes (create/update/delete/publish) use AdminPageWriteService via PageApplicationService so HTML and JSON paths stay aligned.

For translation-enabled page/content lists, grouped translation-family UX is backed by real sibling rows, not only available_locales metadata:

  • Explicit locale filters such as locale=en remain locale-scoped and return only that locale.
  • locale=all, group_by=family_id, and family_id=<id> sibling lookups switch list reads into translation-family expansion mode and emit one row per locale sibling.
  • Grouped panel responses keep the existing children / records family-group contract; the change is upstream row production.

If you bridge go-cms page admin reads through NewGoCMSAdminPageReadAdapter(...), the adapter remains projection-oriented unless the underlying service already expands siblings. Use NewGoCMSAdminPageReadAdapterWithContent(...) when grouped translation-family page lists must expand locale siblings through the CMS content service.

See docs/GUIDE_CMS.md for include flags, locale resolution, blocks payload rules, and view-backed fallback guidance. Migration notes live in CHANGELOG.md.

Commands (go-command)

Commands are message-driven: define a message type with a stable Type() string, implement a command.Commander[Msg], and register a message factory for name-based dispatch from HTTP, RPC, or panels.

New modules should register an owner-scoped set. A set validates every handler and factory, builds an isolated runtime, then publishes the whole owner atomically:

type PublishPageMsg struct {
	IDs []string
}

func (PublishPageMsg) Type() string { return "pages.publish" }

type PublishPageCommand struct {
	store *Store
}

func (c *PublishPageCommand) Execute(ctx context.Context, msg PublishPageMsg) error {
	return c.store.Publish(ctx, msg.IDs)
}

set, err := adm.Commands().NewRegistrationSet("pages")
if err != nil {
	return err
}
if err := admin.RegisterSetCommand(set, &PublishPageCommand{store: store}); err != nil {
	return err
}
if err := admin.RegisterSetContextMessageFactory(set, "pages.publish",
	func(ctx context.Context, payload map[string]any, ids []string) (PublishPageMsg, error) {
		opts, _ := command.DispatchOptionsFromContext(ctx)
		// opts is the same effective snapshot used by the handler and receipt.
		_ = opts.CorrelationID
		return PublishPageMsg{IDs: ids}, nil
	},
); err != nil {
	return err
}
registration, err := set.Commit()
if err != nil {
	return err
}
// Retain registration and call registration.Close() during module shutdown.

RegisterCommand and RegisterMessageFactory remain available for legacy process-global registration. Owned sets are preferable for modules and tests: each committed owner has a private go-command runtime/registry generation, can be committed after global registry startup, and can be closed without disturbing other owners. CommandBus.Reset detaches both owned generations and registrations made through that bus.

Context-aware factories receive the caller's values, deadline, and cancellation plus the normalized effective command.DispatchOptions. Precedence is explicit trusted options, then transport-normalized correlation/idempotency inputs, then permitted server defaults. Payload actor, tenant, or organization fields are never promoted to trusted context; derive authorization and scope from authenticated context.

Configure non-inline capabilities inherited by future owned generations before commit:

err := adm.Commands().SetOwnedRuntimeConfig(admin.OwnedCommandRuntimeConfig{
	Executors: map[command.ExecutionMode]dispatcher.CommandExecutor{
		command.ExecutionModeQueued: queueExecutor,
	},
})

Runner options passed to RegisterSetCommand are handler-local and opt in to in-process retries. Queue delivery retries are a separate policy. Commands that implement go-command descriptor interfaces appear in the command catalog only while their owner is committed; exposure remains opt-in.

CLI/cron metadata is optional via command.CLICommand (CLIOptions) and command.CronCommand (CronOptions).

Command Routing Observability

For queued command flows (for example documents.pdf.remediate), dispatch logs include routing fields:

  • command_id
  • execution_mode
  • dispatch_id
  • correlation_id

See docs/GUIDE_BKG_CMD_OBSERVABILITY.md for dispatch/remediation metrics, alert signals, and activity/audit lifecycle contracts.

FSM Lifecycle Activity Projection

When you use go-command/flow state machines and want lifecycle audit events in the admin-local feed, wire the FSM lifecycle hook to an admin activity sink adapter:

import (
	"context"

	coreadmin "github.com/goliatone/go-admin/admin"
	"github.com/goliatone/go-command/flow"
)

hook := &flow.LifecycleActivityHook[MyMsg]{
	Sink: coreadmin.NewFSMLifecycleActivitySinkAdapter(adm.ActivityFeed()),
}

machine, err := flow.NewStateMachineFromDefinition(
	definition,
	store,
	req,
	resolvers,
	actions,
	flow.WithExecutionPolicy[MyMsg](flow.ExecutionPolicyLightweight),
	flow.WithLifecycleHooks[MyMsg](hook),
	flow.WithHookFailureMode[MyMsg](flow.HookFailureModeFailOpen), // or FailClosed
)
if err != nil {
	panic(err)
}

_, err = machine.ApplyEvent(context.Background(), flow.ApplyEventRequest[MyMsg]{
	EntityID: "entity-1",
	Event:    "approve",
	Msg:      MyMsg{EntityID: "entity-1"},
})

Mapping behavior:

  • LifecycleActivityEnvelope.Verb -> ActivityEntry.Action (for example fsm.transition.attempted|committed|rejected)
  • LifecycleActivityEnvelope.ObjectType + ObjectID -> ActivityEntry.Object
  • Envelope metadata is copied into ActivityEntry.Metadata (including correlation fields like machine_id, machine_version, entity_id, execution_id, event, transition_id, phase)

References

  • Documentation index: docs/README.md
  • Frontend reuse and server-command boundaries: docs/GUIDE_FRONTEND.md
  • Quickstart API and helpers: quickstart/README.md
  • CRUD and DataGrid wiring: docs/GUIDE_CRUD.md
  • Search wiring and go-search integration: docs/GUIDE_SEARCH.md
  • Form generation and go-formgen customization: docs/GUIDE_FORMGEN.md
  • Theme and go-theme integration: docs/GUIDE_THEME.md
  • Workflow/state-machine wiring: docs/GUIDE_WORKFLOW.md
  • RPC transport and command dispatch: docs/GUIDE_RPC.md
  • Authentication, permissions, scope, and debugging: docs/GUIDE_AUTH_PERMISSIONS.md
  • Roles and role seeding: docs/GUIDE_ROLES.md
  • Routing policy contract and CI review workflow: docs/GUIDE_ROUTING.md
  • Example runtime config conventions: examples/web/config/app.json and examples/web/README.md
  • CMS and translation workflow guide: docs/GUIDE_CMS.md
  • Background command routing observability: docs/GUIDE_BKG_CMD_OBSERVABILITY.md
  • Persisted workflow runtime: docs/GUIDE_WORKFLOW.md#persisted-workflow-runtime
  • End-to-end examples: examples/web/main.go, examples/admin-shell/cmd/admin/main.go

Directories

Path Synopsis
commandruntest
Package commandruntest provides transport-neutral contract tests for go-admin command-run transport implementations.
Package commandruntest provides transport-neutral contract tests for go-admin command-run transport implementations.
internal/cmsadapter
Package cmsadapter contains internal CMS adapter helper families that can live outside the top-level admin package without pulling receiver-bound adapter types or admin-only orchestration state with them.
Package cmsadapter contains internal CMS adapter helper families that can live outside the top-level admin package without pulling receiver-bound adapter types or admin-only orchestration state with them.
txoutbox
Package txoutbox provides reusable transaction hook and outbox dispatch helpers.
Package txoutbox provides reusable transaction hook and outbox dispatch helpers.
cmd
Package console contains instance-owned operator console contracts.
Package console contains instance-owned operator console contracts.
examples/datamodule
Package datamodule is the kitchen sink's synthetic dataset provider and managed target.
Package datamodule is the kitchen sink's synthetic dataset provider and managed target.
examples/sqlitestore
Package sqlitestore is a small file-backed reference adapter for lifecycle conformance and host examples.
Package sqlitestore is a small file-backed reference adapter for lifecycle conformance and host examples.
Package debug preserves the Debug panel facade over neutral console primitives.
Package debug preserves the Debug panel facade over neutral console primitives.
examples module
internal
errorutil
Package errorutil classifies internal failures without depending on rendered messages.
Package errorutil classifies internal failures without depending on rendered messages.
modules
services
Package services wires github.com/goliatone/go-services into go-admin.
Package services wires github.com/goliatone/go-services into go-admin.
pkg
admin
Package admin provides the public facade for go-admin.
Package admin provides the public facade for go-admin.
go-deployment-identity
Package deploymentidentity defines deterministic, presentation-safe identities for deployed artifacts.
Package deploymentidentity defines deterministic, presentation-safe identities for deployed artifacts.
go-deployment-identity/identicon
Package identicon provides a deterministic, local PNG deployment persona.
Package identicon provides a deterministic, local PNG deployment persona.
go-lifecycle
Package lifecycle provides framework-neutral startup and shutdown orchestration for hosted Go applications.
Package lifecycle provides framework-neutral startup and shutdown orchestration for hosted Go applications.
go-sync
Package gosync exposes package-level metadata for the go-sync module.
Package gosync exposes package-level metadata for the go-sync module.
go-sync/observability
Package observability contains logs and metrics surfaces for sync packages.
Package observability contains logs and metrics surfaces for sync packages.
go-sync/service
Package service contains sync orchestration services.
Package service contains sync orchestration services.
go-sync/store
Package store contains persistence-facing sync contracts.
Package store contains persistence-facing sync contracts.
go-sync/transport
Package transport contains sync transport helpers and adapters.
Package transport contains sync transport helpers and adapters.
go-translation-ai
Package translationai provides generic AI translation provider contracts, prompt construction, and OpenAI-compatible or Anthropic-compatible providers.
Package translationai provides generic AI translation provider contracts, prompt construction, and OpenAI-compatible or Anthropic-compatible providers.
quickstart module
testkit
admincontract
Package admincontract provides reusable repository contract assertions.
Package admincontract provides reusable repository contract assertions.
tools
facadegen command
translations
ui
ui

Jump to

Keyboard shortcuts

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