Full console demo
Run the console with seeded quota, Stripe, OpenAPI, execution, webhook and audit data. Inspect preview/confirmation/commit behavior and retained execution or delivery records. The demo uses caller-supplied PostgreSQL and creates uniquely named demonstration schemas. It never starts a database service.
Prerequisites
Section titled “Prerequisites”Install Echo Pro with Go 1.26 or later. Supply an existing disposable PostgreSQL 18 database where your assigned login can create and remove its own schemas. Use resources you are authorized to modify. Inject ADMIN_DEMO_POSTGRES_URL at runtime; do not place the connection URL in command arguments or logs. Make loopback port 8080 available.
If your login owns the database but lacks CREATEROLE, the demo uses that one login for everything. Execution and webhook use that one owner, and audit events stay in bounded memory. Durable audit needs its own set of roles and permission to read cluster metadata. Do not grant extra shared privileges merely to make this tutorial pass.
Step 1: start the console
Section titled “Step 1: start the console”Find the installed module directory:
cd "$(go list -m -f '{{.Dir}}' echo.labstack.com/pro)"With the database URL already injected, run:
go run ./examples/admin-demo -addr 127.0.0.1:8080The program checks the listener is a literal loopback IP. It connects to the supplied database and seeds only this run’s admin_demo_... schemas. It prints an admin URL, seed directory URL and fulfillment-console URL. Open http://127.0.0.1:8080/ for the seeded record links and /admin for the payments console. /admin-fulfillment shows the second execution namespace.
Step 2: verify the privilege mode
Section titled “Step 2: verify the privilege mode”Without CREATEROLE, startup reports that role-separation checks and durable PostgreSQL audit are skipped, and that audit records disappear on exit. Treat this as a limitation of this run. Execution and webhook still use the real database, but this mode does not test restricted runtime-role grants.
The privileged fixture path creates its own roles and schemas, applies migrations and cleans up only those names. Use that path only on an independently owned disposable fixture authorized for role changes. It is not required to inspect the ordinary development-owner UI.
Step 3: understand authentication
Section titled “Step 3: understand authentication”If ADMIN_DEMO_API_KEY is absent, browser mode issues an ephemeral local key and injects it only for requests without Authorization. The key stays in memory and is never printed. A supplied invalid credential still fails. Only requests sent to the loopback address get this access.
If ADMIN_DEMO_API_KEY is injected, strict mode requires Authorization: ApiKey <credential> on every request. Configure that mode through a secure client or trusted local proxy. The demo imports the supplied key into a memory store that is discarded at exit. A deployed application stores the StoredKey from IssueKey and keeps old pepper versions.
Step 4: inspect panels and actions
Section titled “Step 4: inspect panels and actions”Open the seeded directory and follow the record links instead of inventing references. Inspect an execution’s holder/send timeline and replay state. Compare webhook queue state with attempt history. Quota usage is seeded in memory; Stripe history is demonstration data and does not dispatch real Stripe usage.
At startup the demo runs real console actions, with CSRF, preview, confirmation and commit. These produce the audit records you see. The demo does not fabricate those admin audit events. Keep the displayed action ID when inspecting a retry. A missing outcome does not establish rollback.
The entry point wires all panels, authentication and lifecycle:
// Command admin-demo runs the full console against disposable PostgreSQL schemas.package main
import ( "context" "crypto/rand" "encoding/hex" "errors" "flag" "fmt" "net" "os" "os/signal" "syscall" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v5"
"echo.labstack.com/pro/admin" "echo.labstack.com/pro/audit" "echo.labstack.com/pro/audit/auditadmin" "echo.labstack.com/pro/audit/auditpg" "echo.labstack.com/pro/execution/pgstore" "echo.labstack.com/pro/openapi/openapiadmin" "echo.labstack.com/pro/webhook/webhookadmin")
func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) }}
func run() error { // Do not put the URL in flag defaults: -help must never disclose credentials. var databaseURL string flag.StringVar( &databaseURL, "postgres-url", "", "disposable PostgreSQL URL (or ADMIN_DEMO_POSTGRES_URL)", ) addr := flag.String("addr", "127.0.0.1:8080", "loopback listen address") flag.Parse() if databaseURL == "" { databaseURL = os.Getenv("ADMIN_DEMO_POSTGRES_URL") } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := runWithContext(ctx, databaseURL, *addr); err != nil { return err } return nil}
func runWithContext(ctx context.Context, databaseURL, addr string) error { if databaseURL == "" { return errors.New( "provide -postgres-url or ADMIN_DEMO_POSTGRES_URL; use the " + "assigned development database with schema creation privileges", ) } host, _, err := net.SplitHostPort(addr) if err != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() { return errors.New("-addr must use a loopback IP and port") } listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) if err != nil { return errors.New("cannot bind demo listen address") } defer func() { if closeErr := listener.Close(); closeErr != nil && !errors.Is( closeErr, net.ErrClosed, ) { fmt.Fprintln(os.Stderr, "demo listener close failed") } }() authority := listener.Addr().String() credential := os.Getenv("ADMIN_DEMO_API_KEY") authentication, err := demoAuthentication(ctx, credential, authority) if err != nil { return err } if credential == "" { fmt.Fprintln( os.Stderr, "browser demo mode: requests without Authorization use an "+ "ephemeral local key", ) } pool, err := pgxpool.New(ctx, databaseURL) if err != nil { return errors.New("invalid PostgreSQL configuration (URL withheld)") } defer pool.Close() if err = pool.Ping(ctx); err != nil { return errors.New( "cannot connect to PostgreSQL (URL and driver diagnostics withheld)", ) } key := make([]byte, 32) if _, err = rand.Read(key); err != nil { return err } prefix := "admin_demo_" + hex.EncodeToString(key[:6]) var canCreateRoles bool if err = pool.QueryRow( ctx, "SELECT rolcreaterole OR rolsuper FROM pg_roles WHERE rolname=current_user", ).Scan(&canCreateRoles); err != nil { return errors.New("cannot inspect demo role privileges") } var roles auditpg.Roles var as demoAuditStore var sink admin.AuditSink if canCreateRoles { roles = auditpg.Roles{ Deployment: prefix + "_owner", Recorder: prefix + "_recorder", Relay: prefix + "_relay", Maintenance: prefix + "_maintenance", Reader: prefix + "_reader", } if err := createRoles(ctx, pool, roles); err != nil { return err } } defer cleanup(ctx, pool, prefix, roles) if canCreateRoles { ac := auditpg.SchemaConfig{Schema: prefix + "_audit", Roles: roles} if err = auditpg.Migrate(ctx, pool, ac); err != nil { return fmt.Errorf("audit migration: %w", err) } store, storeErr := auditpg.New(ctx, pool, ac) if storeErr != nil { return storeErr } as = store sink, err = auditadmin.New(store) } else { fmt.Fprintln( os.Stderr, "development owner mode: execution and webhook use one database "+ "owner; role separation checks and durable PostgreSQL audit are "+ "skipped; audit evidence is in memory and lost on exit", ) store, storeErr := audit.NewMemoryStore(audit.MemoryStoreConfig{ Capacity: audit.Capacity{ Events: 1000, Bytes: 4 << 20, }, }) if storeErr != nil { return storeErr } as = store sink = &memoryAuditSink{store: store, semantics: make(map[string]string)} } if err != nil { return err } auditKey := make([]byte, 32) if _, err = rand.Read(auditKey); err != nil { return err } schema, err := newAuditSchema(auditKey) if err != nil { return err } if err = seedAudit(ctx, as, schema); err != nil { return fmt.Errorf("seed audit: %w", err) } reader, err := audit.NewReader(as, audit.ReaderConfig{Schema: schema, Access: as}) if err != nil { return err } ap, err := auditadmin.NewPanel(auditadmin.PanelConfig{ Reader: reader, EnableReveal: true, }) if err != nil { return err } ec := pgstore.StoreConfig{Schema: prefix + "_execution"} if err = pgstore.Migrate(ctx, pool, ec); err != nil { return fmt.Errorf("execution migration: %w", err) } es, err := pgstore.New(ctx, pool, ec) if err != nil { return err } ep, records, err := seedExecutions(ctx, es, "payments") if err != nil { return fmt.Errorf("seed payments: %w", err) } fp, fulfillment, err := seedExecutions(ctx, es, "fulfillment") if err != nil { return fmt.Errorf("seed fulfillment: %w", err) } ws, err := seedWebhooks(ctx, pool, prefix+"_webhook") if err != nil { return fmt.Errorf("seed webhooks: %w", err) } if err = verifySeededThroughput(ctx, es, ws); err != nil { return fmt.Errorf("seed throughput: %w", err) }
wp, err := webhookadmin.New(webhookadmin.Config{Store: ws, EnableReplay: true}) if err != nil { return err } lim, qp, err := seedQuota(ctx) if err != nil { return err } defer func() { closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer cancel() if closeErr := lim.Close(closeCtx); closeErr != nil { fmt.Fprintln(os.Stderr, "quota close failed") } }() sp, err := seedStripe(ctx) if err != nil { return err } op, err := openapiadmin.New(openapiadmin.Config{}) if err != nil { return err } e := echo.New() cfg := admin.Config{Panels: []admin.Panel{ qp, sp, op, ep, wp, ap, }, Instance: "admin-demo", Keys: [][]byte{key}, Audit: sink, Theme: admin.Theme{ Environment: "Development demo · Operator · writes enabled", Tone: "warning", }, Authenticate: authentication.authenticate, Middleware: authentication.middleware, ActorBoundCSRF: true, Origins: []string{"http://" + demoBrowserAuthority(authority)}, } if err := cfg.Mount(e, "/admin"); err != nil { return err } // executionadmin binds one namespace. A second console exposes the other. cfg.Panels = []admin.Panel{qp, sp, op, fp, wp, ap} if err := cfg.Mount(e, "/admin-fulfillment"); err != nil { return err } if err := mountSamples(ctx, e, lim, op, append(records, fulfillment...)); err != nil { return err } for _, mount := range []string{"/admin", "/admin-fulfillment"} { if err := admin.VerifyRoutes(e, mount); err != nil { return err } } // Exercise the actual console protocol at startup: CSRF, preview, confirmation, // commit and durable intent/outcome evidence, rather than fabricated admin logs. if err = seedAdminActions(ctx, e, authentication.credential, authority); err != nil { return fmt.Errorf("demo action smoke check: %w", err) } return serve(ctx, listener, e, ws)}
func roleNames(r auditpg.Roles) []string { return []string{r.Deployment, r.Recorder, r.Relay, r.Maintenance, r.Reader}}
func createRoles(ctx context.Context, pool *pgxpool.Pool, roles auditpg.Roles) error { tx, err := pool.Begin(ctx) if err != nil { return errors.New("demo role bootstrap failed") } defer rollback(ctx, tx) for _, name := range roleNames(roles) { if _, err = tx.Exec( ctx, "CREATE ROLE "+pgx.Identifier{name}.Sanitize()+" NOLOGIN", ); err != nil { return errors.New( "demo needs a disposable database administrator with CREATE ROLE " + "privileges", ) } } var database, user string if err = tx.QueryRow( ctx, "SELECT current_database(), current_user", ).Scan(&database, &user); err != nil { return errors.New("demo role bootstrap failed") } for _, sql := range []string{ "GRANT " + pgx.Identifier{roles.Deployment}.Sanitize() + " TO " + pgx.Identifier{ user, }.Sanitize(), "GRANT CREATE ON DATABASE " + pgx.Identifier{ database, }.Sanitize() + " TO " + pgx.Identifier{roles.Deployment}.Sanitize(), "GRANT EXECUTE ON FUNCTION pg_catalog.pg_control_system() TO " + pgx.Identifier{ roles.Deployment, }.Sanitize(), } { if _, err = tx.Exec(ctx, sql); err != nil { return errors.New("demo role grants failed") } } return tx.Commit(ctx)}
func rollback(ctx context.Context, tx pgx.Tx) { closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) defer cancel() if err := tx.Rollback(closeCtx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { fmt.Fprintln(os.Stderr, "demo transaction rollback failed") }}
func cleanup( ctx context.Context, pool *pgxpool.Pool, prefix string, roles auditpg.Roles,) { closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second) defer cancel() for _, suffix := range []string{"_execution", "_webhook", "_audit"} { if _, err := pool.Exec( closeCtx, "DROP SCHEMA IF EXISTS "+pgx.Identifier{ prefix + suffix, }.Sanitize()+" CASCADE", ); err != nil { fmt.Fprintln( os.Stderr, "demo schema cleanup failed; remove only this demo run's schemas", ) return } } for _, name := range roleNames(roles) { if name == "" { continue } for _, sql := range []string{ "DROP OWNED BY " + pgx.Identifier{name}.Sanitize(), "DROP ROLE " + pgx.Identifier{name}.Sanitize(), } { if _, err := pool.Exec(closeCtx, sql); err != nil { fmt.Fprintln( os.Stderr, "demo role cleanup failed; stop the disposable fixture to remove it", ) return } } }}The server owns the webhook worker and HTTP shutdown:
package main
import ( "bytes" "context" "encoding/json" "errors" "fmt" "net" "net/http" "net/http/httptest" "os" "time"
"github.com/labstack/echo/v5"
"echo.labstack.com/pro/webhook/webhookpg")
func serve( ctx context.Context, listener net.Listener, e *echo.Echo, store *webhookpg.Store,) error { worker, err := webhookpg.NewWorker(store, webhookpg.WorkerConfig{ Concurrency: 4, ShutdownTimeout: 3 * time.Second, }) if err != nil { return err } work, cancel := context.WithCancel(ctx) defer cancel() workerDone := make(chan error, 1) serverDone := make(chan error, 1) server := &http.Server{ Handler: e, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: time.Minute, MaxHeaderBytes: 16 << 10, } go func() { workerDone <- worker.Run(work) }() go func() { serverDone <- server.Serve(listener) }() fmt.Printf( "Admin demo: http://%s/admin\n"+ "Seed directory: http://%s/\n"+ "Fulfillment console: http://%s/admin-fulfillment\n", listener.Addr(), listener.Addr(), listener.Addr(), ) fmt.Println( "Ctrl-C shuts down the server/worker and removes this run's " + "schemas and roles. Stop any PostgreSQL service you started for " + "this demo.", ) var result error workerJoined, serverJoined := false, false select { case <-ctx.Done(): case result = <-workerDone: workerJoined = true case result = <-serverDone: serverJoined = true } cancel() shutdown, stop := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) defer stop() if err = server.Shutdown(shutdown); err != nil { if closeErr := server.Close(); closeErr != nil { fmt.Fprintln(os.Stderr, "demo server close failed") } result = err } if !workerJoined { if workerErr := <-workerDone; workerErr != nil && !errors.Is( workerErr, context.Canceled, ) { result = workerErr } } if !serverJoined { if serverErr := <-serverDone; serverErr != nil && !errors.Is( serverErr, http.ErrServerClosed, ) { result = serverErr } } if errors.Is(result, context.Canceled) || errors.Is(result, http.ErrServerClosed) { return nil } return result}
func seedAdminActions( ctx context.Context, e *echo.Echo, credential, authority string,) error { if err := consoleAction( ctx, e, credential, authority, "/admin/api/v1/quota/credit-topup", map[string]string{ "tenant": "acme", "plan": "team", "limit": "credits", "amount": "250", "invoice_id": "inv_demo_paid", }, ); err != nil { return err } // Use a separate partition so the reset smoke check preserves review usage. return consoleAction( ctx, e, credential, authority, "/admin/api/v1/quota/reset", map[string]string{ "tenant": "demo-smoke", "plan": "team", "limits": "daily", }, )}
// consoleAction follows the browser protocol against the mounted Echo handler.// Tokens stay in process memory, and responses never reach logs or disk.func consoleAction( ctx context.Context, e *echo.Echo, credential, authority, path string, input map[string]string,) error { request := func(method string, fields map[string]string, into any) error { var body bytes.Buffer if fields != nil { if err := json.NewEncoder(&body).Encode(fields); err != nil { return err } } r := httptest.NewRequestWithContext(ctx, method, "http://"+authority+path, &body) r.Header.Set("Sec-Fetch-Site", "same-origin") r.Header.Set("Content-Type", "application/json") r.Header.Set("Authorization", "ApiKey "+credential) w := httptest.NewRecorder() e.ServeHTTP(w, r) if w.Code != http.StatusOK { var problem struct{ Code string } if err := json.Unmarshal(w.Body.Bytes(), &problem); err != nil { return fmt.Errorf("console %s %s returned HTTP %d", method, path, w.Code) } return fmt.Errorf( "console %s %s returned HTTP %d (%s)", method, path, w.Code, problem.Code, ) } if into != nil { return json.Unmarshal(w.Body.Bytes(), into) } return nil } var form struct{ Controls map[string]string } if err := request(http.MethodGet, nil, &form); err != nil { return err } fields := form.Controls if fields == nil || fields["action_id"] == "" { return errors.New("console did not provide header-authenticated action controls") } for k, v := range input { fields[k] = v } var preview struct { Confirmation string Typed string Acknowledgements map[string]string CommitInput map[string]string `json:"commit_input"` } if err := request(http.MethodPost, fields, &preview); err != nil { return err } if preview.Confirmation == "" { return errors.New("console preview did not provide a confirmation") } for k, v := range preview.CommitInput { fields[k] = v } fields["mode"] = "commit" fields["confirmation"] = preview.Confirmation fields["typed"] = preview.Typed fields["reason"] = "demo_startup_check" for k := range preview.Acknowledgements { fields["ack."+k] = "yes" } return request(http.MethodPost, fields, nil)}Step 5: stop and verify cleanup
Section titled “Step 5: stop and verify cleanup”Press Ctrl-C. The program stops the server and worker, then closes limiter-owned work. It removes only the schemas and roles created by this run. It never drops the supplied database or owner role. An interrupted process can leave its exact run schemas behind; identify them through your authorized database process before cleanup. Do not delete another run’s schemas.
Run the local example tests for the fixture’s authentication and non-database behavior:
go test ./examples/getting-started ./examples/quota-otel ./examples/admin-demoThose tests do not replace PostgreSQL migration, permission or restore checks. A database-backed run needs the supplied backend and reports its own limitations. The browser fixture is useful for inspecting the full UI; it is not a durability or availability benchmark.
Change for deployment
Section titled “Change for deployment”Replace ephemeral credentials with validated application login. Use distinct deployment and runtime roles, exact origins and retained signing keys. Disable actions your operators do not need. Configure a durable audit sink before you enable write actions. Preserve the per-package storage and restore requirements in audit, execution and webhooks.
The admin task guide covers mounting and protocol tests. The configuration reference lists shell defaults; each package reference lists its panel settings. Use problem pages to distinguish failed authorization, denied requests and uncertain mutation outcomes.