Core Interfaces¶
All interfaces are defined in the burrow package (github.com/oliverandrich/burrow).
Required¶
App¶
Every app must implement this interface:
Name()returns a unique identifier for the app (e.g.,"auth","notes")
Apps that don't need configuration only need Name(). Setup logic (initialising repositories, services, etc.) goes in Configure() — see Configurable.
AppConfig¶
Passed to every app's Configure method:
type AppConfig struct {
DB *den.DB
Registry *Registry
Config *Config
WithLocale func(ctx context.Context, lang string) context.Context
}
| Field | Description |
|---|---|
DB |
Den database connection (SQLite with WAL mode, or PostgreSQL) |
Registry |
App registry for looking up other apps |
Config |
Parsed framework configuration |
WithLocale |
Function that returns a new context with the given locale set (provided by the i18n Bundle) |
Optional¶
Apps can implement any combination of these interfaces. The framework detects them via type assertion and calls the appropriate methods during the boot sequence.
HasDocuments¶
Returns a slice of document type instances that Den should register. Called during startup before Configure(). Den inspects each type's struct tags and creates or updates the underlying collections and indexes automatically:
See the Migrations guide for details on schema management.
HasRoutes¶
Registers HTTP handlers on the Chi router. Called after all apps are registered.
func (a *App) Routes(r chi.Router) {
r.Route("/notes", func(r chi.Router) {
r.Get("/", burrow.Handle(a.handleList))
r.Get("/{id}", burrow.Handle(a.handleDetail))
r.Group(func(r chi.Router) {
r.Use(auth.RequireAuth())
r.Post("/", burrow.Handle(a.handleCreate))
})
})
}
See the Routing guide for details on handlers, URL parameters, and middleware.
HasMiddleware¶
Returns middleware functions applied globally to the router. Applied in app registration order.
func (a *App) Middleware() []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
a.sessionMiddleware,
}
}
HasNavItems¶
Returns navigation entries collected into the request context by the framework:
type NavItem struct {
Label string // displayed and also used as the i18n message ID
URL string
Icon string // template-define name (e.g. "auth/icon_people"), empty for no icon
Position int
AuthOnly bool
AdminOnly bool
}
Layouts render Icon via the {{ icon "..." }} template function, which looks up the named template define (see Template Functions). Each contrib owns its icons in templates/icons.html as {{ define "<app>/icon_<name>" }} blocks.
func (a *App) NavItems() []burrow.NavItem {
return []burrow.NavItem{
{
Label: "Notes",
URL: "/notes",
Position: 20,
AuthOnly: true,
},
}
}
See the Navigation guide for positioning and ordering.
HasTemplates¶
Returns an fs.FS containing .html template files. Templates must use {{ define "appname/templatename" }} blocks to namespace themselves. All template files from all apps are parsed into a single global *template.Template at boot time.
//go:embed templates/*.html
var templateFS embed.FS
func (a *App) TemplateFS() fs.FS {
sub, _ := fs.Sub(templateFS, "templates")
return sub
}
See the Layouts & Rendering guide for details on template rendering and layout wrapping.
HasFuncMap¶
Returns a static template.FuncMap added at parse time. Functions are available globally in all templates. The framework panics if two apps register the same function name.
Functions are global — don't register twice
Once an app registers a function, it is available in all templates across all apps. If your app depends on another app that already registers a function (e.g., icon functions), use it directly in your templates — do not re-register it in your own FuncMap(). Duplicate registration causes a panic.
To avoid name collisions, prefix custom functions with your app name (e.g., notesFormatDate instead of formatDate). This is especially important for icon functions where a collision would silently swap one icon for another.
func (a *App) FuncMap() template.FuncMap {
return template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
}
}
Reserved function names
The following names are owned by the framework or shipped by standard contribs. Even if your build doesn't register the providing contrib, avoid these names in your own FuncMap — the server will panic at startup once the contrib is added.
Always available (core base / always-on i18n bundle):
safeHTML, safeURL, safeAttr, add, sub, pageURL, pageNumbers, icon, dict, lang, t, tData, tPlural, navItems, navLinks. Also mediaURL when Den is opened with a Storage.
Reserved for standard contribs (registered only when the contrib is registered; templates using them otherwise fail to parse at boot):
staticURL (staticfiles), csrfToken / csrfField / csrfHxHeaders (csrf), messages (messages), currentUser / isAuthenticated / authLogo / credName / deref (auth), naturaltime / intcomma / filesizeformat (humanize).
HasRequestFuncMap¶
Returns context-scoped template functions that are injected per-request via template.Clone(). Use this for functions that depend on the context (e.g., current user, CSRF token, locale). The context.Context parameter enables template rendering both inside HTTP handlers (where the context comes from the request) and outside them (background jobs, SSE broadcasts) via RenderFragment.
func (a *App) RequestFuncMap(ctx context.Context) template.FuncMap {
return template.FuncMap{
"currentUser": func() *User {
return CurrentUser(ctx)
},
"isAuthenticated": func() bool {
return CurrentUser(ctx) != nil
},
}
}
HasFlags¶
Returns CLI flags merged into the application's flag set. The configSource parameter enables TOML file sourcing — pass nil when no config file is used.
func (a *App) Flags(configSource func(key string) cli.ValueSource) []cli.Flag {
return []cli.Flag{
&cli.IntFlag{
Name: "notes-page-size",
Value: 20,
Usage: "Number of notes per page",
Sources: burrow.FlagSources(configSource, "NOTES_PAGE_SIZE", "notes.page_size"),
},
}
}
Configurable¶
Called after CLI parsing to read flag values and initialise the app. Receives the shared AppConfig (database, registry, config) and the parsed CLI command for reading flag values. All setup logic that needs database access or flag values belongs here.
func (a *App) Configure(cfg *burrow.AppConfig, cmd *cli.Command) error {
a.repo = NewRepository(cfg.DB)
a.pageSize = int(cmd.Int("notes-page-size"))
return nil
}
See the Configuration guide for the three-tier config system.
HasCLICommands¶
Returns CLI subcommands (e.g., set-role, create-invite). Wire them into the top-level cli.Command via srv.CLICommands():
cmd := &cli.Command{
Name: "myapp",
Flags: srv.Flags(nil),
Action: srv.Run,
Commands: srv.CLICommands(), // boots DB + Configure before each subcommand
}
srv.CLICommands() wraps each subcommand's Action with the server's boot sequence (open DB, run Configure() on all apps) and tears it down afterwards, so a subcommand like set-role alice admin runs against a fully-configured app graph. Use srv.Registry().AllCLICommands() only as a low-level escape hatch when you need the raw, unwrapped commands and want to manage the boot lifecycle yourself.
func (a *App) CLICommands() []*cli.Command {
return []*cli.Command{
{
Name: "seed-notes",
Usage: "Create sample notes for testing",
Action: func(ctx context.Context, cmd *cli.Command) error {
return a.seedNotes(ctx)
},
},
}
}
HasMigrations¶
type HasMigrations interface {
Migrations() []NamedMigration
}
type NamedMigration struct {
Version string
Migration migrate.Migration
}
Contributes versioned, run-once database migrations. The server applies them automatically at boot via Den's migrate package — each migration runs exactly once across processes, tracked in the _den_migrations collection. Versions are namespaced by app name ({app}/{version}) so two contribs can both ship "001_initial" without colliding. The Forward function receives a transaction, so the migration's writes are atomic.
import (
"github.com/oliverandrich/den"
"github.com/oliverandrich/den/migrate"
)
func (a *App) Migrations() []burrow.NamedMigration {
return []burrow.NamedMigration{{
Version: "001_initial_categories",
Migration: migrate.Migration{
Forward: func(ctx context.Context, tx *den.Tx) error {
for _, c := range defaultCategories {
if err := den.Save(ctx, tx, &c); err != nil {
return err
}
}
return nil
},
},
}}
}
The Backward function is optional; provide it when the migration is meaningfully reversible. Burrow only wires the forward path at boot — if you need rollback, reach for den/migrate's Down / DownOne runners directly (e.g. wrap them in your own HasCLICommands subcommand). There is no built-in burrow migrate down CLI.
HasStaticFiles¶
Contributes static file assets that the staticfiles app collects and serves. The prefix namespaces files under the static URL path (e.g., prefix "auth" serves files at /static/auth/...). Files are content-hashed and cache-busted just like user-provided static files.
//go:embed static
var staticFS embed.FS
func (a *App) StaticFS() (string, fs.FS) {
sub, _ := fs.Sub(staticFS, "static")
return "myapp", sub
}
HasAdmin¶
Contributes admin panel routes and navigation items. AdminRoutes receives a Chi router already prefixed with /admin and protected by auth middleware. The admin contrib app discovers all HasAdmin implementations and mounts them.
AdminAuth¶
type AdminAuth interface {
RequireAuth() func(http.Handler) http.Handler
RequireStaff() func(http.Handler) http.Handler
RequireAdmin() func(http.Handler) http.Handler
}
Provides authentication and authorization middleware for the admin panel. The admin contrib app discovers an AdminAuth provider from the registry during Configure and uses RequireAuth + RequireStaff to gate the /admin/ frame; individual HasAdmin apps add RequireAdmin on top of admin-only routes. contrib/auth implements this interface — custom auth systems must provide all three methods (admin implies staff implies authenticated).
func (a *App) AdminRoutes(r chi.Router) {
r.Get("/notes", burrow.Handle(a.adminListNotes))
r.Get("/notes/{id}", burrow.Handle(a.adminNoteDetail))
}
func (a *App) AdminNavItems() []burrow.NavItem {
return []burrow.NavItem{
{Label: "Notes", URL: "/admin/notes", Position: 30},
}
}
See the Admin contrib app for the full admin panel setup.
HasTranslations¶
Contributes translation files for the i18n app. The returned fs.FS must contain TOML files (e.g., active.en.toml, active.de.toml). The i18n app auto-discovers all HasTranslations implementations at startup.
//go:embed translations
var translationFS embed.FS
func (a *App) TranslationFS() fs.FS { return translationFS }
See the i18n guide for translation file format and usage.
HasDependencies¶
Returns app names that must be registered before this app. NewServer automatically sorts apps by dependencies. The registry panics at startup if any dependency is missing.
HasJobs¶
Registers background job handlers with the job queue. The queue implementation (e.g., contrib/jobs) discovers all HasJobs apps during its PostConfigure() phase and calls RegisterJobs on each one. Because PostConfigure() runs after all Configure() calls, your app can safely use state set in Configure() inside RegisterJobs.
Use typed task definitions for compile-time safety, or the raw q.Handle() for dynamic job types:
// Typed (recommended):
var cleanupTask = burrow.DefineTask("notes.cleanup", handleCleanup)
func (a *App) RegisterJobs(q burrow.Queue) {
cleanupTask.Register(q)
}
// Raw:
func (a *App) RegisterJobs(q burrow.Queue) {
q.Handle("notes.cleanup", a.handleCleanup)
a.jobs = q // store as burrow.Enqueuer for later enqueueing
}
PostConfigurable¶
Runs a second configuration pass after all Configurable.Configure() calls have completed. This is useful when an app needs to interact with other apps' state that is only available after Configure() — for example, contrib/jobs uses PostConfigure() to discover and register HasJobs handlers from other apps.
Most apps do not need this interface. Prefer Configurable unless you specifically need cross-app coordination that depends on post-Configure state.
Startable¶
Called after the full boot sequence completes — templates built, middleware and routes registered — but before the HTTP listener starts. This is the counterpart to HasShutdown: use Startable to launch background processes and HasShutdown to stop them.
The *Server parameter gives access to server resources like TemplateExecutor() that are only available after boot. For example, contrib/jobs implements Startable to create its worker pool with the template executor, so job handlers can use RenderFragment:
func (a *App) Start(srv *burrow.Server) error {
a.worker = NewWorker(a.repo, a.handlers, a.workerCfg, srv.TemplateExecutor())
ctx, cancel := context.WithCancel(context.Background())
a.cancelFunc = cancel
go a.worker.Start(ctx)
return nil
}
Most apps do not need this interface. Use it only when you need to start background goroutines that depend on the fully initialized server.
HasShutdown¶
Performs cleanup during graceful shutdown (e.g., stopping background goroutines, flushing buffers). Called in reverse registration order before the HTTP server stops. Errors are logged but do not prevent other apps from shutting down. The context carries the server's shutdown timeout.
func (a *App) Shutdown(_ context.Context) error {
close(a.stopCh) // signal background worker to stop
return nil
}
ReadinessChecker¶
Contributes to the readiness probe at /healthz/ready (provided by the healthcheck app). Return nil when the app is ready to serve traffic, or an error describing what is not ready. The healthcheck app iterates all registered ReadinessChecker apps and reports their status.