README
¶
go-admin Web Example
A complete reference implementation demonstrating the go-admin library with production-ready integrations including go-auth authentication, activity tracking, settings management, and multi-tenant support.
Features
This example demonstrates:
- ✅ Authentication & Authorization via go-auth
- ✅ Activity Tracking with hooks pattern and dashboard integration
- ✅ User, Role, Profile & Preferences Management powered by go-users on shared SQLite (panels, search, onboarding, go-crud JSON APIs)
- ✅ Multi-Tenant Support with organizations and tenant isolation
- ✅ Dynamic Settings with hierarchical scopes (system/site/user)
- ✅ CMS Integration with widget areas and navigation menus
- ✅ Command Pattern for actions, jobs, and CLI operations
- ✅ Search Engine with typeahead and cross-resource queries
- ✅ Notification System with unread tracking
- ✅ Advanced Sidebar Navigation with collapse, groups, separators, submenus, and LabelKey/GroupTitleKey i18n
- ✅ Panel CRUD for Users, Pages, Posts, Media with filtering and bulk actions
- ✅ Job Registry with cron metadata for scheduling
- ✅ Theme System with dynamic tokens and variant support
Quick Start
Prerequisites
- Go 1.21+
- Node.js 18+ (for Tailwind CSS build)
Running the Example
# From the repo root
cd pkg/client/assets
# Install frontend dependencies (Tailwind + TS build)
npm install
# Build CSS + JS bundles
npm run build
# Run the server
cd ../../examples/web
go run .
# Run the server
go run .
The server starts at http://localhost:8080/admin
If you are iterating on the quickstart submodule locally, make sure the root
module resolves it via either:
- a
replace github.com/goliatone/go-admin/quickstart => ./quickstartentry in the rootgo.mod, or - a
go.workfile that includes./quickstart.
API Endpoints
- Health:
GET /admin/health - Dashboard HTML (SSR):
GET /admin/dashboard- Server-side rendered dashboard with inline state - Dashboard JSON:
GET /admin/api/dashboard- JSON API (backwards compatible) - Navigation:
GET /admin/api/navigation - Settings:
GET /admin/api/settings,POST /admin/api/settings - Search:
GET /admin/api/search?query=... - Notifications:
GET /admin/api/notifications - Activity:
GET /admin/api/activity?limit=50&offset=0&channel=users(filters:user_id,actor_id,verb,object_type,object_id,channel/channels,channel_denylist,since,until,q; response includesentries,total,next_offset,has_more; defaults: limit 50, max 200, offset 0; ordered by most recent first). - Jobs:
GET /admin/api/jobs,POST /admin/api/jobs/:name/trigger - Users Panel:
GET /admin/api/users,POST /admin/api/users, etc. - Roles Panel:
GET /admin/api/roles,POST /admin/api/roles, etc. - User Actions:
/admin/api/users/:id/{activate,suspend,disable,archive,reset-password,invite}plus/admin/api/users/bulk/{activate,suspend,disable,archive,assign-role,unassign-role}(mirrored under/admin/crud/users/...). - Profile & Preferences:
GET/POST /admin/api/profile,GET/POST /admin/api/preferences - Onboarding:
POST /admin/api/onboarding/invite,POST /admin/api/onboarding/password/reset/request,POST /admin/api/onboarding/password/reset/confirm,POST /admin/api/onboarding/register(flagged) - Users CRUD API:
GET /admin/crud/users(go-crud JSON endpoints) - Tenants Panel:
GET /admin/api/tenants - Organizations Panel:
GET /admin/api/organizations - Session:
GET /admin/api/session(current authenticated user snapshot)
Stage 1 quickstart helpers
If you want a minimal Stage 1 admin (login + dashboard only), the quickstart helpers provide a smaller wiring surface:
WithFeatureDefaults(DefaultMinimalFeatures())to keep a minimal gate default set.WithAdapterFlags(config.Admin.AdapterFlags)to drive adapter wiring from config (env fallback still available).NewModuleRegistrarusesadm.FeatureGate()by default (passWithModuleFeatureGates(customGate)to override).WithGoAuth(...)to wire auth + authorizer in one call.WithDefaultDashboardRenderer(...)for a basic SSR dashboard (override templates viaWithDashboardTemplatesFS).
Preferences quickstart
adm, err := quickstart.NewAdminWithGoUsersPreferences(
cfg,
deps.PreferenceRepo,
quickstart.EnablePreferences(),
)
if err != nil {
return err
}
_ = adm
If /admin/api/preferences returns 403, grant admin.preferences.view and admin.preferences.edit.
Content UI (Pages, Posts, Media)
- HTML CRUD screens live at
/admin/pages,/admin/posts,/admin/media(auth + permissions enforced:admin.pages.*,admin.posts.*,admin.media.*). - Actions include create/edit/delete plus publish/unpublish (pages) and publish/archive (posts); media supports add/edit/delete metadata.
- Navigation highlights the active item; search results link to the new views.
Content persistence (SQLite + go-repository-bun)
- CRUD APIs (
/admin/crud/{pages,posts,media}) are backed by SQLite via go-repository-bun stores; go-cms migrations from../go-cms/data/sql/migrationsare applied on startup with a light overlay that createsadmin_pages,admin_posts, andmediademo tables for the example flows. - Configure the DSN with
CONTENT_DATABASE_DSN(preferred), falling back toCMS_DATABASE_DSN, elsefile:/tmp/go-admin-cms.db?cache=shared&_fk=1; stores seed once when the tables are empty using the demo fixtures. - Controllers also register plural aliases (
/admin/crud/posts,/admin/crud/posts/:id,/admin/crud/posts/batch, etc.) so the DataGrid and HTML flows keep using plural paths while hitting the DB. - Smoke: create/edit/delete a page, post, and media item via the UI or
/admin/crud/{resource}; restart the server and confirm the records persist and still filter/sort in the lists.
Sidebar Navigation & Quickstart defaults
- Navigation can be seeded via
quickstart.SeedNavigationinexamples/web/setup/navigation.go(setUSE_NAV_SEED=true); by default the app uses module menu contributions withquickstart.EnsureDefaultMenuParentsso grouped/collapsible nav renders without seeding. Menus are addressed by slug (cfg.NavMenuCode) for deterministic IDs; reset persistent menus withRESET_NAV_MENU=trueor delete/tmp/go-admin-cms.dbwhen switching sources. - Sidebar templates/assets come from quickstart embeds (collapse + submenu persistence); override by layering your own template/assets FS via
quickstart.NewViewEngineoptions inexamples/web/main.go. - Menu items include both
Label/LabelKeyandGroupTitleKey; modules can nest under seeded groups viaParentID. - Collapse state persists (
admin-sidebar-collapsed), submenu state persists per submenu key, andNAV_DEBUG=trueexposes the ordered nav JSON in the sidebar (NAV_DEBUG_LOG=truelogs payload). - Logo shrinks in collapsed mode; separators and group titles remain visible.
Template Functions
- The view engine uses
quickstart.DefaultTemplateFuncs, which includessingularize,pluralize,toJSON,dict,formatNumber, and widget title helpers. These are helpers (globals) in Pongo2, so call them like{{ singularize(resource_label|default:resource)|title }}. - Example-specific widget title labels are configured in
examples/web/helpers/template_funcs.goviahelpers.TemplateFuncOptions(). - To add custom template functions while keeping defaults, use
quickstart.MergeTemplateFuncsand pass the result toquickstart.WithViewTemplateFuncsinexamples/web/main.go.
Sidebar Manual QA
- Toggle collapse/expand, refresh, and confirm state persists.
- Expand/collapse
ContentandMy Shop; child links persist across reloads. - Group titles and separators render once after permission filtering; no duplicate children on reseed.
- In collapsed mode, text hides while icons stay aligned; submenu chevrons hide until expanded.
- With
NAV_DEBUG=true, verify nav JSON matches rendered order; withNAV_DEBUG_LOG=true, payload logs once per build.
Session Widget & API
- Sidebar footer now renders the current session from go-auth (
helpers.BuildSessionUserviahelpers.WithNav), showing display name/email plus role/tenant when available; guest state falls back to “Not signed in.” - Authenticated snapshot is also available at
GET /admin/api/session(mirrors the footer payload: id/subject/email/username/role/tenant/org/resource_roles/scopes/issued_at/expires_at). - QA: login as admin/editor/viewer and confirm the footer avatar initial and labels match the account; collapse the sidebar to hide text; call
/admin/api/sessionwith the issued token and expect 200 with populated fields; without/expired token the auth middleware returns 401 and the footer shows Guest/Not signed in.
Adding a new CRUD resource (HTML)
- Routes & guards: define routes under
/admin/<resource>with auth middleware and permission checks (admin.<resource>.read/create/edit/delete). Seehandlers/users.goandhandlers/tenants.go. - URL helper: use
helpers.NewResourceRoutes(basePath, resource)to buildindex/new/show/edit/deleteURLs. Avoid hardcoding strings. - Handlers: populate generic view keys:
resource,resource_label,routes,items(list),columns(list headers),resource_item(detail/form),fields(detail sections),is_edit,form_action,form_method. Attachactionsper row viaroutes.ActionsMap(id). - Templates: place views at
pkg/client/templates/resources/<resource>/list.html,detail.html,form.html. The generic templates for users/tenants show how to render the shared keys. - Data shape: return maps with snake_case fields; keep consistent keys across resources so the same template structure works.
- Smoke: list/create/edit/delete, verify guards (401/403 for unauthorized), and ensure menu visibility matches permissions.
User Detail Tabs (Example)
- Users detail renders a tab strip sourced from
schema.tabsin the admin detail payload. - Tabs are registered for the
userspanel inexamples/web/modules.goand include Activity + Profile. - The detail handler (
examples/web/handlers/users.go) calls the admin detail API and maps tabs to view links; the template renderstabs. - If you change the tab targets, ensure the routes exist (e.g.,
/admin/activity) or update the target to a panel/path that does. - Tab content is resolved by a host-app resolver (
helpers.TabContentResolver) and render mode selector (helpers.TabRenderModeSelector) to support SSR, hybrid, or client-only strategies. - Inline tabs (content kinds:
details,dashboard_area,cms_area,template) link back to the detail URL with?tab=<id>, while navigation tabs keep theirpanel/path/externaltargets. - The detail handler reads
?taband setsactive_tab(defaults todetails) for consistent deep linking. - Hybrid tabs fetch HTML from
/admin/users/:id/tabs/:tabon demand; client tabs fetch JSON from/admin/api/users/:id/tabs/:tab. - The detail template includes a lightweight loader that swaps the tab panel for
data-render-mode="hybrid|client"tabs and updates the URL to preserve deep links.
Authentication
go-auth Integration
The example uses production-ready go-auth integration via setup/auth.go:
func SetupAuth(adm *admin.Admin, dataStores *stores.DataStores) {
cfg := demoAuthConfig{signingKey: "web-demo-secret"}
provider := &demoIdentityProvider{users: dataStores.Users}
auther := auth.NewAuthenticator(provider, cfg)
routeAuth, err := auth.NewHTTPAuthenticator(auther, cfg)
// Wire go-auth to admin
adm.WithAuth(admin.NewGoAuthAuthenticator(routeAuth, cfg), &admin.AuthConfig{
LoginPath: "/admin/login",
LogoutPath: "/admin/logout",
RedirectPath: "/admin",
})
// Wire authorization
adm.WithAuthorizer(admin.NewGoAuthAuthorizer(admin.GoAuthAuthorizerConfig{
DefaultResource: "admin",
}))
}
Demo Authentication Tokens
When the server starts, it logs JWT tokens for demo users:
demo Authorization tokens (use Authorization: Bearer <token>):
- admin (admin): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- editor (editor): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- viewer (guest): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Use these tokens in the Authorization header:
curl -H "Authorization: Bearer <token>" http://localhost:8080/admin/api/dashboard
User management (go-users + go-crud)
- Users/roles/profiles/preferences are backed by go-users on the shared SQLite DSN (
CMS_DATABASE_DSN); seeds create admin/editor/viewer/inactive with passwords<username>.pwd, and demo JWTs are printed from the DB on startup. - go-auth reads users from go-users and issues resource roles for
admin.users.*,admin.roles.*,admin.profile.*,admin.preferences.*(admin → owner, editor → member, viewer → none); profile/preferences remain self-service even when broader perms are absent. - Panels (
/admin/api/users|roles|profile|preferences), navigation, search, and/admin/crud/usersall run through the same scope guard and permissions; unauthorized tokens get 403s and the menu hides. - Onboarding flags live in
main.go(users.invite✅,users.password_reset✅,users.signup❌ by default with allowlist mode). Override withUSE_USER_INVITES,USE_PASSWORD_RESET,USE_SELF_REGISTRATION,REGISTRATION_MODE(open|allowlist|closed), andREGISTRATION_ALLOWLIST. Endpoints sit under/admin/api/onboarding/*for invite, accept/verify, reset request/confirm, and optional self-registration. - Lifecycle transitions, admin-triggered invites/password resets, and bulk role assign/unassign live under
/admin/api/users/*(also/admin/crud/users/*) and emituserschannel activity for the dashboard widget/search feeds. - Lifecycle actions (activate/suspend/disable/archive) and role assignment emit activity to the
userschannel and surface in the dashboard activity widget; preferences persist via go-usersPreferenceRepository. - Quick wiring/seed notes live in
docs/prds/EXAMPLE_USERS_TDD.md; the smoke checklist is indocs/prds/EXAMPLE_SMOKE.md.
Onboarding + Secure Links
- Securelink env vars:
ADMIN_SECURELINK_KEY,ADMIN_SECURELINK_BASE_URL,ADMIN_SECURELINK_QUERY_KEY,ADMIN_SECURELINK_AS_QUERY,ADMIN_SECURELINK_EXPIRATION. - The example falls back to a demo signing key when
ADMIN_SECURELINK_KEYis unset (seeexamples/web/setup/securelink.go). - Default securelink paths:
/admin/invite,/admin/register,/admin/password-reset/confirm(base path mirrorsADMIN_BASE_PATH). - UI routes:
/admin/password-reset(request) and/admin/password-reset/confirm(apply token). Securelink reset URLs land on the confirm page. - API endpoints remain under
/admin/api/onboarding/*; UI routes are registered inexamples/web/main.gowith custom view context for token parsing and policy hints. - Errors follow the go-errors response shape with
error.text_code(seedocs/GUIDE_ONBOARDING.mdfor the canonical list).
JSON CRUD Smoke (Users)
Use the demo tokens to exercise the go-crud JSON endpoints (snake_case payloads) backed by the same go-users SQLite store:
- List users:
curl -H "Authorization: Bearer <token>" http://localhost:8080/admin/crud/users - Create:
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"username":"api.user","email":"api.user@example.com","role":"editor","status":"active"}' http://localhost:8080/admin/crud/user - Update:
curl -X PUT -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"status":"inactive"}' http://localhost:8080/admin/crud/user/<id> - Delete:
curl -X DELETE -H "Authorization: Bearer <token>" http://localhost:8080/admin/crud/user/<id>
Swapping Identity Providers
The demoIdentityProvider can be replaced with any implementation of auth.IdentityProvider:
type IdentityProvider interface {
VerifyIdentity(ctx context.Context, identifier, password string) (Identity, error)
FindIdentityByIdentifier(ctx context.Context, identifier string) (Identity, error)
}
For production, replace with a database-backed provider:
// Example: PostgreSQL-backed provider
type PostgreSQLIdentityProvider struct {
db *sql.DB
}
func (p *PostgreSQLIdentityProvider) VerifyIdentity(ctx context.Context, identifier, password string) (auth.Identity, error) {
// Query database, verify password hash
// Return user identity
}
Dashboard Rendering (SSR + Hydration)
The example demonstrates hybrid server-side rendering with client-side hydration for optimal performance and user experience.
Architecture
- Server-Side Rendering (SSR): Complete HTML is rendered server-side with widgets and data
- Client Hydration: JavaScript attaches behaviors to existing DOM without re-rendering
- Zero API Fetches: Initial page load requires no additional API calls
- Backwards Compatible: JSON API (
/admin/api/dashboard) remains available
How It Works
- Server renders complete HTML at
/admin/dashboardusing Go templates - Inline state is embedded as JSON in the page for hydration
- WidgetGrid client attaches drag-and-drop, resize, and visibility behaviors
- Layout changes persist via AJAX to
/admin/api/dashboard/preferences
Performance Benefits
- <200ms initial render (server-side, no API waterfall)
- Instant interactivity (behaviors attach to existing DOM)
- SEO-friendly (fully rendered HTML)
- Reduced bandwidth (single request vs. multiple API calls)
Implementation
The dashboard renderer is wired in main.go:
// Wire dashboard renderer for server-side rendering
dashboardRenderer, err := setup.NewDashboardRenderer()
if err != nil {
log.Printf("warning: failed to initialize dashboard renderer (falling back to JSON API): %v", err)
} else {
if dashboard := adm.Dashboard(); dashboard != nil {
dashboard.WithRenderer(dashboardRenderer)
log.Println("Dashboard SSR enabled")
}
}
Template: pkg/client/templates/dashboard_ssr.html
Renderer: renderers/dashboard_renderer.go
Hydration: assets/src/dashboard/widget-grid.ts
Routes
- SSR:
GET /admin/dashboard- Full HTML page with inline state - JSON API:
GET /admin/api/dashboard- JSON payload (backwards compatible) - Preferences:
GET/POST /admin/api/dashboard/preferences- Layout persistence
Customizing Templates
The renderer uses Go's html/template with custom functions:
toJSON- Serialize data for inline stategetWidgetTitle- Human-readable widget titlesformatNumber- Locale-aware number formattingsafeHTML- Render trusted HTML
Example widget template:
{{define "widgetContent"}}
{{if eq .definition "admin.widget.user_stats"}}
<div class="metrics">
{{$values := index .data "values"}}
{{range $key, $value := $values}}
<div class="metric">
<small>{{$key}}</small>
<span>{{formatNumber $value}}</span>
</div>
{{end}}
</div>
{{end}}
{{end}}
Disabling SSR
To revert to JSON-only rendering, simply don't wire the renderer:
// Comment out renderer wiring in main.go
// dashboardRenderer, err := setup.NewDashboardRenderer()
The dashboard will automatically fall back to the JSON API endpoint.
Dashboard Persistence
The dashboard uses goliatone/go-dashboard with CMS-backed persistent storage. Widgets and user layout preferences are stored in the database and survive server restarts.
Prerequisites:
- CMS persistence must be enabled (
USE_PERSISTENT_CMS=true) - Dashboard feature enabled (
Features.Dashboard = true, default)
Run with persistent dashboard:
USE_PERSISTENT_CMS=true go run .
Expected behavior:
- Dashboard widgets persist across server restarts
- Layout preferences saved to database via go-options or go-users
- Widget data fetched from CMS widget store
- Activity hooks integrated with dashboard events
- Console log shows:
Dashboard: go-dashboard (persistent, requires CMS)
Verify integration:
# Start server
USE_PERSISTENT_CMS=true go run . &
# Test JSON API
curl -s http://localhost:8080/admin/api/dashboard | jq '.areas[0].widgets[0].id'
# Test HTML SSR
curl -s http://localhost:8080/admin | grep "dashboard-state"
# Test preferences persistence
curl -s -X POST http://localhost:8080/admin/api/dashboard/preferences \
-H "Content-Type: application/json" \
-d '{"areas":{"admin.dashboard.main":[{"id":"widget-1","span":6}]}}'
Activity Tracking
Activity Hooks Pattern
The example demonstrates the activity hooks pattern with bidirectional flow:
- Admin Activity Feed → Dashboard Hooks
- Command Execution → Dashboard Hooks
- Panel CRUD Operations → Activity Sink
See main.go for the composite activity sink setup; env flags can swap the primary sink:
USE_GO_USERS_ACTIVITY=trueenables the go-users sink when available; otherwise falls back to in-memory.USE_PERSISTENT_CMS=trueswaps to the persistent CMS via the quickstart adapter hook; falls back to in-memory.USE_GO_OPTIONS=trueswaps the settings backend to go-options; falls back to in-memory settings.
dashboardHooks := dashboardactivity.Hooks{
dashboardactivity.HookFunc(func(ctx context.Context, event dashboardactivity.Event) error {
log.Printf("[Dashboard Activity] %s %s %s:%s",
event.ActorID, event.Verb, event.ObjectType, event.ObjectID)
return nil
}),
}
dashboardCfg := dashboardactivity.Config{
Enabled: true,
Channel: "admin",
}
compositeActivitySink := quickstart.NewCompositeActivitySink(
adm.ActivityFeed(),
dashboardHooks,
dashboardCfg,
)
adm.WithActivitySink(compositeActivitySink)
Activity Events
Commands emit activity events via the hooks pattern:
type UserActivateCommand struct {
store *stores.UserStore
activityHooks activity.ActivityHookSlice
}
func (c *UserActivateCommand) Execute(ctx admin.AdminContext) error {
// ... perform activation ...
c.activityHooks.Notify(ctx.Context, activity.Event{
Channel: "users",
Verb: "activated",
ObjectType: "user",
ObjectID: userID,
Data: map[string]any{
"email": user.Email,
"name": user.Name,
},
})
return nil
}
Activity backends & smoke
- Flag:
USE_GO_USERS_ACTIVITY=trueswaps the activity sink to the go-users adapter (examples/web/setup/activity.go) while keeping dashboard hooks and the in-memory fallback buffer. - Smoke (in-memory): start normally, create a page (
POST /admin/api/pages), update a post (PUT /admin/api/posts/<id>), delete a media item (DELETE /admin/api/media/<id>), thenGET /admin/api/activity?limit=10and confirmentriesshowpage:<id>,post:<id>,media:<id>with actor + metadata. - Smoke (go-users): restart with the flag, repeat the same create/update/delete calls, and verify
/admin/api/activitystill returns the new events inentrieswith intact IDs/actors; toggle the flag off again to confirm the feed keeps working.
Persistent CMS (go-cms + Bun/SQLite)
- Flag:
USE_PERSISTENT_CMS=trueswaps the CMS container to go-cms using the Bun storage adapter (examples/web/setup/cms_persistent.go). Default DSN isfile:/tmp/go-admin-cms.db?cache=shared&_fk=1; override withCMS_DATABASE_DSN. - Migrations: applied automatically at startup via go-persistence-bun using the embedded go-cms SQL migrations (no manual migration step needed).
- Requirements: SQLite driver (sqliteshim is bundled; no additional install) and write access to the DSN path.
- Smoke:
USE_PERSISTENT_CMS=true go run ./examples/web→ logs showCMS backend: go-cms (sqlite).- Call
GET /admin/api/navigationand confirm menus load; stop the server, restart with the same DSN, and confirm the menu payload still resolves (data persisted). - Optional: point
CMS_DATABASE_DSNto a different file path and confirm a fresh database is created with the same migrations applied.
Content (Pages + Posts)
- Pages and Posts panels now swap to the go-cms backend when
USE_PERSISTENT_CMS=true(using the container fromexamples/web/setup/cms_persistent.go); with the flag off they use the in-memory stores seeded with demo content. - Navigation seeds a
Contentparent guarded byadmin.pages.*/admin.posts.*; the go-auth role provider now issues matching resource roles so the menu and panels hide for unauthorized tokens. - CMS-backed stores emit
page:<id>/post:<id>activity (actor + slug/status metadata) and drive search adapters from go-cms data, keeping search and activity aligned with the persistent backend. - Smoke:
USE_PERSISTENT_CMS=true CMS_DATABASE_DSN=file:/tmp/go-admin-cms.db?cache=shared&_fk=1 go run ./examples/web, create/edit/delete a page and a post via/admin/pagesand/admin/posts, confirm/admin/api/search?query=<slug>returns them and/admin/api/activity?limit=5shows create/update/delete inentrieswith correct actors; turn the flag off to fall back to the seeded in-memory content without errors.
User & Role Management
The example includes a complete user management module (admin/users.go) with:
UserManagementService- orchestrates CRUD and role assignmentsUserRepositoryandRoleRepositoryinterfacesInMemoryUserStore- default in-memory implementationGoUsersUserRepositoryandGoUsersRoleRepository- go-users adapters
Using In-Memory Store (Default)
service := admin.NewUserManagementService(nil, nil)
// Uses InMemoryUserStore for both users and roles
Using go-users Adapters
See ../../docs/prds/ADMIN_REFACTOR.md#role-assignment-lookup-system-vs-custom-roles for context on filtering system roles from custom role assignments.
import users "github.com/goliatone/go-users/pkg/types"
authRepo := // your go-users AuthRepository
inventory := // your go-users UserInventoryRepository
roleRegistry := // your go-users RoleRegistry
userRepo := admin.NewGoUsersUserRepository(authRepo, inventory, scopeResolver)
roleRepo := admin.NewGoUsersRoleRepository(roleRegistry, scopeResolver)
service := admin.NewUserManagementService(userRepo, roleRepo)
service.WithRoleAssignmentLookup(admin.UUIDRoleAssignmentLookup{})
service.WithActivitySink(adm.ActivityFeed())
Multi-Tenant Support
The example seeds demo tenants and organizations:
if svc := adm.TenantService(); svc != nil {
tenant, _ := svc.SaveTenant(ctx, admin.TenantRecord{
Name: "Acme Corp",
Slug: "acme",
Status: "active",
Domain: "acme.local",
Members: []admin.TenantMember{
{UserID: "user-1", Role: "owner"},
},
})
}
Panels are registered for tenants and organizations:
/admin/api/tenants- Tenant CRUD/admin/api/organizations- Organization CRUD
Settings Management
Dynamic settings with hierarchical scopes (system/site/user):
- Default in-memory backend:
setup.SetupSettings(adm)viasetup/settings.go - go-options backend: set
USE_GO_OPTIONS=trueto route throughsetup.SetupSettingsWithOptions(adm)insetup/settings_options.go(adds go-options scope metadata + snapshot IDs, seeds a site override forfeatures.release_channel)
Endpoints:
GET /admin/api/settings/form- Form schema with current values + go-options scopesPOST /admin/api/settings- Update settings with validation
Go-options flag smoke checklist:
USE_GO_OPTIONS=true go run ./examples/webthen hit/admin/api/settings/form→scopesincludesource: go-optionsand snapshot IDs;features.release_channelshowsscope: site- POST
/admin/api/settingswith{ "values": { "performance.cache_ttl": -1 }, "scope": "site" }→400withfields.performance.cache_ttlerror andmetadata.scopein response - POST
/admin/api/settingswith{ "values": { "features.release_channel": "stable" }, "scope": "site" }→ subsequent GET returnsprovenance: siteand the new value
Modules
The example demonstrates the module pattern:
Registering Modules
type usersModule struct {
store *stores.UserStore
}
func (m *usersModule) Manifest() admin.ModuleManifest {
return admin.ModuleManifest{
ID: "users",
NameKey: "modules.users.name",
DescriptionKey: "modules.users.description",
}
}
func (m *usersModule) Register(ctx admin.ModuleContext) error {
// Register panels, commands, search providers
return nil
}
adm.RegisterModule(&usersModule{store: dataStores.Users})
Built-in Modules
- Users Module - user management panel with activate/deactivate commands
- Pages Module - CMS pages with publish/unpublish
- Posts Module - blog posts with categories
- Media Module - file uploads with metadata
- Tenants Module - multi-tenant management
- Organizations Module - organization hierarchy
- Preferences Module - user preferences
- Profile Module - user profile management
Command Pattern
Commands are first-class operations triggered via:
- Panel actions (single record)
- Bulk actions (multiple records)
- Jobs (scheduled/manual)
- CLI (if implementing
CommandWithCLI)
Implementing Commands
type UserActivateCommand struct {
store *stores.UserStore
activityHooks activity.ActivityHookSlice
}
func (c *UserActivateCommand) Name() string {
return "users:activate"
}
func (c *UserActivateCommand) Execute(ctx admin.AdminContext) error {
userID := ctx.Context.Value("user_id").(string)
// ... perform activation ...
c.activityHooks.Notify(ctx.Context, activity.Event{...})
return nil
}
// Optional: CLI metadata
func (c *UserActivateCommand) CLIMetadata() admin.CLIMetadata {
return admin.CLIMetadata{
Path: "users:activate",
Description: "Activate a user account",
Flags: []admin.CLIFlag{
{Name: "user-id", Type: "string", Required: true},
},
}
}
Registering Commands
activateCmd := commands.NewUserActivateCommand(m.store).
WithActivityHooks(activityAdapter)
ctx.Admin.Commands().Register(activateCmd)
Extending the Example
Adding a New Module
- Create module struct implementing
admin.Module - Implement
Manifest()andRegister(ctx admin.ModuleContext) - Register panels, commands, search providers in
Register() - Wire activity hooks if needed
- Register module in
main.go
Adding a New Panel
builder := ctx.Admin.Panel("my-resource").
WithRepository(myRepo).
ListFields(
admin.Field{Name: "name", Label: "Name", Type: "text"},
admin.Field{Name: "status", Label: "Status", Type: "select"},
).
FormFields(
admin.Field{Name: "name", Label: "Name", Type: "text", Required: true},
).
WithFilters(
admin.Filter{Field: "status", Label: "Status", Type: "select", Options: statusOptions},
).
WithActions(
admin.Action{
Name: "my-action",
Label: "My Action",
Command: "my-resource:my-action",
},
)
Swapping Adapters
The example uses in-memory implementations by default. Swap with production adapters:
Authentication: Replace demoIdentityProvider with database-backed provider
Settings: Wire go-options registry (Phase 18 adapter available)
Activity: Set USE_GO_USERS_ACTIVITY=true to use the go-users ActivityLogger adapter (Phase 17) defined in setup/activity.go. The example wires the read path with admin.Dependencies{ActivityRepository: usersDeps.ActivityRepo, ActivityAccessPolicy: activity.NewDefaultAccessPolicy()} in examples/web/main.go so /admin/api/activity uses the go-users policy.
CMS: Wire go-cms persistent container (Phase 20 adapter available)
Architecture
Directory Structure
examples/web/
├── main.go # Application entry point
├── modules.go # Module registration
├── commands/ # Command implementations
│ ├── user_commands.go
│ ├── page_commands.go
│ ├── post_commands.go
│ └── media_commands.go
├── handlers/ # HTTP handlers
├── helpers/ # Utility functions
├── jobs/ # Job definitions
├── pkg/activity/ # Activity hooks package
├── search/ # Search providers
├── setup/ # Setup functions
│ ├── auth.go # go-auth integration
│ ├── dashboard.go # Dashboard widgets
│ └── settings.go # Settings definitions
├── stores/ # Data stores (in-memory)
├── pkg/client/templates/ # HTML templates
└── openapi/ # OpenAPI specs
pkg/client/assets/ # Admin client assets (source + dist)
Integration Points
- Authenticator:
admin.WithAuth(authenticator, authConfig) - Authorizer:
admin.WithAuthorizer(authorizer) - Activity Sink:
admin.WithActivitySink(sink) - Theme Provider:
admin.WithThemeProvider(provider) - CMS Container:
admin.Config.CMS.Container
Production Considerations
Security
- Replace demo signing key: Use environment variable for JWT secret
- HTTPS: Enable TLS in production
- CORS: Configure allowed origins
- Rate Limiting: Implement per-user rate limits
- Session Storage: Use Redis/PostgreSQL instead of in-memory
Database
- Persistent Storage: Replace in-memory stores with PostgreSQL/MySQL
- Migrations: Use go-migrate or similar
- Connection Pooling: Configure appropriate pool sizes
Monitoring
- Activity Logging: Ship activity events to log aggregator
- Metrics: Export Prometheus metrics
- Tracing: Integrate OpenTelemetry
Deployment
- Environment Variables: Externalize configuration
- Health Checks: Monitor
/admin/healthendpoint - Graceful Shutdown: Handle SIGTERM signals
- Horizontal Scaling: Ensure session state is shared
References
- go-admin Core Documentation
- ADMIN_TDD.md - Architecture decisions
- ADMIN_TSK.md - Implementation tasks
- DASH_TDD.md - Dashboard SSR technical design
- DASH_TSK.md - Dashboard SSR implementation plan
- go-auth - Authentication library
- go-users - User management
- go-options - Settings management
- go-dashboard - Dashboard widgets
License
See root LICENSE file
Documentation
¶
There is no documentation for this package.