Changelog¶
All notable changes to Burrow are documented here. The format is based on Keep a Changelog.
0.30.0 — 2026-06-05¶
Breaking Changes¶
tasks.EnqueuergainsEnqueueBatchandEnqueueBatchAt. CustomEnqueuer/Queueimplementations (including test mocks) must add both methods; apps usingcontrib/jobsare unaffected. See the v0.30 migration guide.
Added¶
- Batch enqueueing:
EnqueueBatch/EnqueueBatchAt. N jobs of one type insert in a single all-or-nothing transaction (one commit instead of N) — for fan-out callers like one delivery job per follower. OnEnqueuer/Queue, the typed task wrappers, andcontrib/jobs; see the Jobs guide.
Changed¶
- Den bumped to v0.17.1. Bugfix release: enabling FTS on an already-populated SQLite collection now backfills the index, so rows saved before the first
den:"fts"registration become searchable without a re-save.
0.29.1 — 2026-06-04¶
Fixed¶
- Reverse-proxy forwarded headers no longer corrupt the request URL (regression from 0.29.0). Behind a trusted proxy reporting
X-Forwarded-Proto, the middleware setr.URL.Schemeon an origin-form server request (no host), sor.URL.String()becamehttps:///path— polluting request logs and any absolute URL built fromr.URL.r.URLis now left untouched; read the proxied scheme viaburrow.RequestIsHTTPS(r)as before.
0.29.0 — 2026-06-04¶
Added¶
- contrib/jobs reports worker liveness through
/healthz/ready. The jobs app now implementsReadinessChecker, returning 503 with ajobsentry when the poller stalls so orchestrators stop routing to an instance that can no longer drain its queue. - contrib/jobs admin: worker-status panel. The
/admin/jobspage now shows a card with the worker pool's state (running / stopped / stalled), worker count, in-flight jobs, last poll time, and per-status job counts. - Reverse-proxy forwarded-headers support (
--forwarded-mode, defaultprivate). Behind a TLS-terminating proxy the framework now derives the request scheme from a trusted proxy'sX-Forwarded-Proto, gated on the direct TCP peer:private(default) trusts loopback + RFC1918 — covering same-host nginx/Caddy with zero config — whileloopback,trusted-cidrs(with--forwarded-trusted-cidrs), andofftune the trust boundary. Newburrow.RequestIsHTTPS(r)reports the per-request scheme. Directly-served requests are unaffected — only requests whose TCP peer is loopback/RFC1918 consult the header. See the Reverse Proxy guide.
Changed¶
- contrib/session and contrib/secure honor the proxied request scheme. Session cookies are now marked
Securefor HTTPS requests detected viaburrow.RequestIsHTTPS(r), even when the app's base URL is plainhttp(upgrade-only — an https base URL is never downgraded).contrib/secureemits HSTS for those requests only when--forwarded-modeis explicitly enabled. - contrib/csrf derives the request scheme per-request. The CSRF origin/referer check now treats a request as HTTPS via
burrow.RequestIsHTTPS(r)(which honors a trusted proxy'sX-Forwarded-Proto) instead of the boot-time base-URL scheme. Behind a reverse proxy this fixes the403 "origin invalid"that rejected browser POSTs whoseOriginwashttpswhile the app saw plain HTTP. The CSRF cookie's ownSecureattribute still follows the base-URL scheme (gorilla/csrf sets it once at startup).
Fixed¶
- Background loops no longer crash the process on panic. The auth orphaned-user/email-token sweep, the WebAuthn session sweep, the rate-limiter eviction sweep, the jobs poller, and the SIGHUP graceful-restart handler now recover per-iteration panics (logged with a stack trace) instead of letting the goroutine panic propagate unrecovered.
0.28.0 — 2026-06-04¶
Added¶
- contrib/apidocs: vendored Scalar OpenAPI documentation UI.
apidocs.New(apidocs.WithSpecURL(…), apidocs.WithTitle(…))serves an interactive API reference page (default/api/docs) pointed at an OpenAPI spec — thecrudSpecHandlerroute by default. The Scalar bundle is vendored and served content-hashed throughstaticfileswith web fonts disabled, so the docs work fully offline. See the API Docs guide. crudOpenAPI: security schemes and documentation prose. Declare authentication so the spec advertises it:api.AddSecurityScheme("bearerAuth", crud.BearerAuth("token"))plusapi.Secured("bearerAuth")(document-level; multiple names are OR alternatives), withBearerAuth/APIKeyAuthhelpers that avoid importing kin-openapi. New resource options add prose a viewer can show:WithTag(tag name + description),WithActionDoc(per-action summary + description), andWithSecurity(per-resource override; no names = public). Security is descriptive only — authentication stays the host's middleware.
0.27.0 — 2026-06-03¶
Breaking Changes¶
- contrib/auth: the
Rendererinterface andWithRendereroption are removed. Auth pages now render directly through their named templates; customize a page by redefining its template block ({{ define "auth/login" }}…, last-define-wins) instead of implementing a renderer.WithAuthLayoutis unchanged. Migrating off a customRenderermeans deleting theauth.WithRenderer(...)call and moving any markup changes into template overrides.
Added¶
crudpackage: declarative JSON CRUD APIs.crud.NewResource[T](db, opts…)turns a Den document type into the six standard endpoints (list/get/create/update via PATCH partial-merge/replace via PUT full-replace/delete) as a plainhttp.Handler— mount it withr.Mount, or register it into a route group withRoutes(r)to add custom sibling actions. Options cover ownership scoping (WithScope, applied to every action), typed write models (WithCreate/WithUpdate, mass-assignment-safe), output shaping (WithPresenter), default sorting, allowlisted client-driven filtering/ordering/search (WithFilter/WithOrdering/WithSearch, each ANDed with the scope so it only narrows;WithFullTextSearchruns?search=as a relevance-ranked Den FTS query overden:"fts"columns instead of a LIKE scan), optimistic concurrency (WithOptimisticConcurrencymaps Den's_revto ETags and enforcesIf-Matchon writes — 428 if missing, 412 if stale), forward cursor pagination (WithCursorPagination—?after=<id>with anext_cursor, id-keyset, no COUNT, an alternative to the offset default), relation expansion (WithExpandable—?expand=authorinlines an allowlistedden.Linkrelation as a nested object instead of its id, batched, get + list), and action subsetting (Only/Except). Auth and CSRF stay the host's ordinary middleware. See the JSON CRUD APIs guide.crudOpenAPI 3.0 spec generation.crud.NewAPI(info)collects resources —api.Mount(r, path, res)mounts and records in one call — and serves an OpenAPI 3.0 document viaapi.SpecHandler(): every endpoint with its parameters (pagination/filter/ordering/search/expand), request/response schemas reflected from your Go types (withvalidatetags mapped to constraints), and the error envelope.BaseURLbecomes the spec's server so paths stay relative. Built on kin-openapi.- contrib/auth: API-key (personal access token) bearer auth. The automatic user-loading middleware now also resolves an
Authorization: Bearer <token>header, so non-browser API clients authenticate through the existing gates (RequireAuth/RequireStaff/RequireAdmin) with no extra middleware — a bearer request populates the same context as a session cookie. Those gates now answer API clients (Accept: application/json) with a 401 instead of a login redirect. Tokens arebrw_-prefixed, stored only as their SHA256 hash (plaintext shown once), and managed through the auth repository (CreateAPIKey/ListAPIKeysByUser/DeleteAPIKey). Bearer API routes must be declared CSRF-exempt viacsrf.ExemptPaths. - contrib/auth: self-service API-key page at
/auth/api-keys. Authenticated users list, create (plaintext shown once), and revoke their own keys. Link to it from your account UI; restyle it by redefining theauth/api_keystemplate. pagination.CursorResult+PageResult.NextCursor. ACursorResult(hasMore, nextCursor)helper and anext_cursorJSON field for forward cursor pagination, mirroringOffsetResult/page; re-exported asburrow.CursorResult. The field isomitempty, so existing offset responses are byte-for-byte unchanged.
Changed¶
- Den bumped to v0.17.0. It ships the FTS literal-by-default
Search, full-replacePreserveServerFields, and linkMarshal/LinkFields/selectiveWithFetchLinksthat the newcrudpackage builds on. The re-exportedden.*alias surface is API-compatible — no downstream changes required.
Security¶
- Go toolchain pinned to 1.26.4. Picks up the patched standard library for GO-2026-5037 (
crypto/x509) and GO-2026-5039 (net/textproto). The requiredtestCI check stays on1.26; the lint job'sgovulncheckruns on 1.26.4.
0.26.0 — 2026-05-31¶
Breaking Changes¶
--ratelimit-trust-proxyremoved. Client-IP extraction now lives at server level via--client-ip-mode; ratelimit readsburrow.ClientIP(r.Context()). Migration depends on your proxy (Nginx, Cloudflare, AWS ALB, etc.) — see the Client IP guide for the right combination. The old behaviour matched Nginx withngx_http_realip_moduleand is now--client-ip-mode=header --client-ip-header=X-Real-IP.
Added¶
- Framework-wide client IP via chi v5.3.0's new
ClientIPFromXAPI. Four--client-ip-modechoices (remote-addr,header,xff-trusted-proxies,xff-trusted-cidrs), each with its companion flag — boot fails fast on missing/mismatched companions.burrow.ClientIP(ctx)andburrow.ClientIPAddr(ctx)expose the value to contribs without a chi import. ratelimit is the first consumer; future request-logging / geofencing / abuse-detection can read from the same source. - contrib/auth:
<noscript>fallback in the public auth layout. Login/register/recovery pages render a translated noscript block so JS-off browsers see an explanation instead of a silently-broken passkey button. New i18n keysauth-noscript-{title,body,link-text}ship in English + German. - contrib/auth:
WithUsernameValidator/WithEmailValidatorregistration hooks. Reject otherwise-free-but-unwanted usernames or email addresses at signup (reserved handles, blocked addresses) — the validator runs before user creation and its error message is shown to the user. Uniqueness stays enforced by the database. See Registration Validators.
Changed¶
- contrib/admin + contrib/auth: form templates commit to
hx-post. Admin user-edit, recovery-codes ack, and admin logout drop theirmethod=postno-JS fallbacks —contrib/authis WebAuthn-only and admin sits behindRequireAuth + RequireStaff, so those paths were unreachable. The examplenotesapp follows the same pattern. No runtime behaviour change. See admin → JavaScript required.
0.25.6 — 2026-05-25¶
Added¶
csrf.ExemptPathscapability interface for apps that own webhook routes (Webmention inbound, ActivityPub inbox, payment callbacks). An app returns its exempt paths fromCSRFExemptPaths() []stringand the csrf app discovers every implementor at boot, merges the declarations into a single matcher, and bypassesgorilla/csrf.UnsafeSkipCheckfor matching requests. Keeps the declaration local to the app that owns the route —main.gostays unaware. Patterns are minimal by design: exact ("/webmention") and prefix ("/inbox/").
0.25.5 — 2026-05-25¶
Changed¶
burrow devdefault watch extensions now include.toml,.yml, and.yamlalongside the existing.go/.html/.css. Edits to translation bundles (*.toml) and config files now trigger a rebuild without an explicit--watch-extsoverride — fixes the silent failure mode where a translation-file fix didn't recover from a boot-time i18n check failure until the dev server was restarted by hand.
0.25.4 — 2026-05-25¶
Security¶
- Scaffold CI:
cache: falseon the check job'smise-actionstep to close zizmor'scache-poisoningfinding. The release job (which depends oncheckvianeeds:) builds release artifacts from the same toolchain — leaving the check-job cache enabled would let an untrusted PR branch poison build inputs that flow into a later release. Matches burrow's own.github/workflows/ci.yml, which already hadcache: falseon every mise-action call.
0.25.3 — 2026-05-25¶
Added¶
auth.WithDefaultRole(role)constructor option. Sets the role assigned to newly-registered users afterCreateUsersucceeds, so apps where every accepted invitee should be staff don't have to wrap the registration flow or run a post-hoc CLI. The first-user →RoleAdminpromotion still wins (a fresh deployment never locks itself out of/admin/), and an invalid role string makesConfigurereturn an error at boot. Leaving the option unset preserves the historicRoleUserdefault.mise run cleanandmise run clean-alltasks in burrow itself and the project scaffold.cleanremoves build artifacts and generated files (coverage outputs,bin/,tmp/,site/, docs build output,.tailwind/source dirs, generated example static bundles);clean-alladditionally wipes every gitignoreddata/runtime dir and every*.db*SQLite file anywhere in the tree.clean-alldepends oncleanso there's no copy-paste drift.docs/getting-started/scaffold.mddocuments the output ofburrow new— annotated file tree, full mise-task reference, dev/release loops, and what the scaffold deliberately omits. The Quick Start tip box, the index Minimal Example, and the Tooling CLI section now cross-link to it;tooling.mdkeeps the CLI-flag reference, the new page covers the generated project.
Fixed¶
burrow devno longer kills a freshly-restarted child on a stale exit observation. When the app exited (compile error, panic) and the user saved a file before the supervisor's exit-monitor goroutine had finished its cleanup, the cleanup would cancel the brand-new child's context, killing it. The dev server recovered on the next save but the experience looked like "save → crash → nothing rebuilds." The exit handler now refuses to act on observations from a previous generation (internal/dev/supervisor.go).
0.25.2 — 2026-05-24¶
Changed¶
- Scaffold + examples: enable
@tailwindcss/typography+@tailwindcss/formsvia@plugindirectives intailwind.css. Both plugins ship bundled with the standalone Tailwind CLI (no JS toolchain ornode_modulesinvolved). Tree-shaken when unused — zero CSS cost for projects that don't touch.proseor form controls.@tailwindcss/aspect-ratiois also bundled but intentionally left off: Tailwind v4's built-inaspect-*utilities supersede it. docs/guide/releases.mdrefreshed. Removed stale mentions ofjust, links to the archivedgo-burrow-templaterepo, and an inline Homebrew Cask recipe the scaffold never shipped. Inline goreleaser + CI snippets replaced by pointers to the scaffold's own.goreleaser.yamland.github/workflows/ci.ymlso the page stops drifting from the source of truth.- Scaffold: release job moved into
ci.ymlas a tag-only stage (if: startsWith(github.ref, 'refs/tags/v'),needs: [check, zizmor]). Standalonerelease.ymlis removed. Matches burrow's own workflow shape and ensures release builds run against the same CI gate asmainpushes. - Scaffold: goreleaser builds FreeBSD and OpenBSD binaries for
amd64andarm64, alongside the existing Linux / macOS / Windows targets. Possible because the scaffold is consistentlyCGO_ENABLED=0. (NetBSD was evaluated butmodernc.org/sqliteupstream has a compile bug fornetbsd/amd64and ships no source fornetbsd/arm64; left out until that's fixed.)
0.25.1 — 2026-05-24¶
Fixed¶
contrib/selfupdatearchive extraction now picks tar.gz vs zip from the asset's actual filename instead ofruntime.GOOS. The previous heuristic assumed the burrow-scaffold convention (zip on darwin/windows) and brokeWithAssetMatcherusers whose archives don't follow it —goreleaser_Darwin_all.tar.gz, for example, was opened as a zip and failed with "not a valid zip file". Burrow-scaffold-shaped releases are unaffected.
0.25.0 — 2026-05-24¶
Upgrading from v0.24? See the v0.25 migration guide.
Added¶
contrib/selfupdate— in-appupdatecommand. Adds anupdatesub-command to burrow binaries that fetches the latest release from GitHub, Codeberg, or any Forgejo instance, verifies the archive against the release'schecksums.txtSHA256, extracts the binary, and atomically replaces the running executable viaminio/selfupdate. Anonymous public-API access only — no tokens. Defaults match the scaffold's.goreleaser.yamlasset layout; override viaWithAssetPattern(template) orWithAssetMatcher(predicate).WithBinaryNamecovers the case where the archive's binary doesn't match the repo name. Verifies the SHA256 of the archive before parsing it, skips tar symlink entries (won't get tricked into writing a 0-byte binary), enforces a per-binaryflockagainst concurrent updates, caps download sizes, escapes URL path segments, and surfaces rate-limit responses with the reset timestamp. Linux is the tested target; macOS works as a side effect, Windows is best-effort (no flock).burrow dev— integrated dev server. New CLI sub-command runs the app viago rununder anfsnotify-backed file watcher. On each debounced file change it sequentially rebuilds the Tailwind CSS bundle (viaburrow tailwind, when configured) and restarts the app — the Go rebuild re-embeds the fresh CSS via//go:embed. Replaces the Air-based loop in the project scaffold; auto-discovers the entry-point and Tailwind paths from the conventional burrow layout. Detects app crashes (build errors, panics, port-in-use) and logs them. Linux is the tested target; macOS works as a side effect, Windows is best-effort.burrow devauto-creates.envwithSESSION_HASH_KEYandCSRF_KEY(mode 0600) on first run when the file is missing; existing files are read verbatim via godotenv and never patched. Shell env vars win over.envvalues (standard 12-factor precedence). Pass--no-env-fileto skip both.
Changed¶
- Project scaffold no longer ships
.air.tomlor pinsairin.mise.toml.mise run devbecomesgo tool burrow dev;mise run setupnow callsgo tool burrow dev --init-envto materialise.env..gitignoretracks.envinstead of.dev-keys. - Den bumped to v0.16.1. Bugfix release: top-level
Or()no longer swallows sibling AND-predicates (SQL precedence fix); SQLitetime.Time,[]byte, andjson.RawMessagecomparison operators now match the JSON storage encoding.
0.24.0 — 2026-05-23¶
Upgrading from v0.23? See the v0.24 migration guide.
Breaking Changes¶
Registrysplit into storage and lifecycle. Storage (Add,Get,Apps, name lookup) moves into a newgithub.com/oliverandrich/burrow/registrypackage as free functions; the lifecycle orchestration (ConfigureAll,Configure,RegisterMiddleware,RegisterRoutes,RunMigrations,RegisterDocuments,AllFlags,AllNavItems,AllAdminNavItems,AllCLICommands,Shutdown) is no longer exported —Serverorchestrates them internally. Construct registries withregistry.New(), add apps withregistry.Add(reg, app).burrow.App,burrow.Registry, andburrow.HasDependenciesremain available as type aliases pointing at the new package, so existingburrow.Appreferences compile unchanged.
Added¶
- Typed registry lookup.
registry.Get[T](reg)returns(T, bool)for Optional-Service lookups (graceful degradation when the provider may be missing).registry.MustGet[T](reg)returnsTand panics on a missing or ambiguous type — the idiomatic shape for Hard-Dependency apps that declare the provider inDependencies().registry.GetByName(reg, name)andregistry.MustGetByName(reg, name)cover the string-keyed Soft-Discovery case (used bycontrib/adminto discover itsAdminAuthprovider).
Changed¶
docs/guide/inter-app-communication.mddocuments three lookup patterns — Hard-Dependency, Optional-Service, Soft-Discovery — with the registry function each one uses and the failure mode it carries.sse.BrokerFromRegistrysimplified toregistry.Get[*App]. Behaviour unchanged (still returns nil when SSE is not registered); the old name-lookup-plus-type-assert is gone.contrib/jobsdocs corrected to "Den-backed (SQLite + Postgres)". Doc comments and READMEs called it "SQLite-backed" — that was true in v0.1 but the repository has been Postgres-aware since Den arrived. Code behaviour unchanged.burrowpackage split into themed public sub-packages. Implementation moves intoburrow/app(App interface, capability interfaces, AppConfig, NavItem, Config and sub-configs, context helpers),burrow/server(Server, boot sequence, templates, listener, TLS, OpenDB),burrow/web(HandlerFunc, HTTPError, Handle, JSON, Render, Bind, Validate),burrow/tasks(Queue, Task, Result, JobConfig),burrow/pagination(PageRequest, PageURL, OffsetResult). The burrow root retains type aliases and wrapper functions for the everyday API —burrow.App,burrow.AppConfig,burrow.HasRoutes,burrow.NewServer,burrow.Handle,burrow.JSON,burrow.Render,burrow.Bind,burrow.Validate,burrow.DefineTask,burrow.ParsePageRequestand so on continue to work via aliases. Less-frequent operations are also reachable via direct sub-package import (tasks.DefineTask[P],pagination.ParsePageRequest,app.Configsub-configs,web.HandlerFunc).Config.resolvedTLSModeis now exported asResolvedTLSModebecausetls.golives inburrow/serverand calls it across the package boundary. No behaviour changes.- Den bumped to v0.16.0. Internal refactor in Den (its
internal/coresplit into public sub-packages); theden.Xalias surface burrow uses is API-compatible — no changes required. burrow.gosplit into themed root files for readability, with smoke tests added to the root package. No API change.
0.23.0 — 2026-05-21¶
Upgrading from v0.22? See the v0.23 migration guide.
Breaking Changes¶
- Three-tier role model in
contrib/auth.RoleStaff = "staff"joinsRoleUserandRoleAdmin; admins are implicit staff (User.IsStaff()true for both). The/admin/frame now opens to staff and admins viaRequireAuth + RequireStaff. HasAdminapps self-gate admin-only routes. Wrap them inr.Group(func(r chi.Router) { r.Use(auth.RequireAdmin()); … })insideAdminRoutes. Built-incontrib/authandcontrib/jobsalready do this.burrow.AdminAuthinterface gainsRequireStaff(). Custom providers must add the method (one-line stub: returnRequireAdmin()to preserve v0.22 frame semantics).auth promote/auth demoteCLI removed. Replaced byauth set-role <username> <user|staff|admin>with role validation.
Added¶
burrow.NavItem.StaffOnlyflag sitting next toAuthOnly/AdminOnly.burrow.IsAuthenticated/IsStaff/IsAdminhelpers plusAuthChecker.IsStaffso app code can query auth state without importingcontrib/auth.auth.RequireStaff()middleware for staff-only routes outside/admin/(e.g. a/studioshell).
0.22.1 — 2026-05-21¶
Fixed¶
contrib/authrecovery flow auto-regenerates on last-code consumption. WhenRecoveryLoginconsumes the user's final unused code, the handler now routes through the/auth/recovery-codesack flow so passkey-only accounts can't end up atremaining_codes: 0without a fresh safety net. Happy path withremaining > 0is unchanged.
0.22.0 — 2026-05-20¶
Migrating from v0.21.x? See the v0.22 migration guide for the concrete before/after patterns.
Breaking Changes¶
-
Seedableinterface and--seedflag removed; replaced byHasMigrationswired to Den's migrate package. Apps that previously implementedSeed(ctx) errornow implementMigrations() []burrow.NamedMigration, returning versioned migrations whoseForwardfunctions take a*den.Tx. The server applies them automatically at boot — each migration runs exactly once across processes (tracked in the_den_migrationscollection), namespaced by app name ({app}/{version}). Migration recipe: move the oldSeedbody into amigrate.Migration{Forward: func(ctx, tx) error { ... }}and useden.Save(ctx, tx, doc)/den.NewQuery[T](tx, ...)directly inside Forward — repos keep their*den.DBshape; migrations are setup code that operates in the framework-persistence layer. Drop the--seed/SEED=trueinvocation — booting is enough. All six tutorial-step polls apps migrated alongside. -
--i18n-default-languageand--i18n-supported-languagesflags removed. The supported locale set is now derived from eachHasTranslationsapp'sTranslationFS():Bundle.AddTranslationsextends the matcher with every locale it loads fromactive.<locale>.tomlfilenames. The bundle's source-language tag is hardcoded to English — with the Label-as-key convention, the literal you write in{{ t "..." }}IS the source string, so a German-first project just writes German keys, ships onlyactive.de.toml, and needs no framework configuration.i18n.NewBundle()andi18n.NewTestBundle(translationFSes...)lose their leading args. Apps that previously set--i18n-*flags fail loudly with urfave/cli's unknown-flag error — migrate by deleting them. -
NavItem.LabelKeyandChoice.LabelKeyremoved. Labels now double as the i18n message ID —navLinkspipesNavItem.Labelthroughi18n.Tdirectly, so contribute translations keyed by the English Label (e.g.Users = "Benutzer") instead of an opaqueadmin-nav-userskey. On a translation miss the raw Label is rendered. Built-in contribs (auth,jobs) and theexample/notesapp migrate their translation TOMLs accordingly. See navigation and i18n: Labels as Keys. auth.Useris now generic —auth.User[P any]. TheProfiletype parameter holds app-specific extension fields (display name, bio, avatar, social links, …) stored inline as a nested JSON object on the user document. Apps without extensions parametrise withauth.EmptyProfile.auth.New()→auth.New[auth.EmptyProfile]();auth.Repository→auth.Repository[auth.EmptyProfile];auth.CurrentUser(ctx)→auth.CurrentUser[auth.EmptyProfile](ctx). TheNameandBiofields are removed from User core — apps that want a display name define aProfile.Nameand render{{ .User.Profile.Name }}with{{ .User.Username }}as a fallback.Repository.CreateUserandCreateUserWithEmailno longer take anameparameter;Repository.SearchUserssearches Username and Email only (Profile-field search lives in app code). Theauthtestpackage hard-codesauth.EmptyProfile; apps with a custom Profile maintain their own seeding helpers. Existing user records in the database load fine — missingprofilekeys unmarshal to the zero value of P. See Extending the User for the full migration recipe.den:tags on Profile fields (index, unique, fts, index_together) are honoured via Den 0.14's nested-struct schema walk.auth.Configurerejectsdocument.Base-embedding Profile types at startup, since Profile is serialised inline and is not a Den document.
Changed¶
--auth-webauthn-rp-idfalls back to the base URL's hostname instead of hardcoding"localhost". Local dev withhost=localhost port=8080still resolves tolocalhost(port stripped); deployed apps with--base-url=https://app.example.comnow correctly derive RPIDapp.example.comwithout needing to set the flag. Pass the flag explicitly to override for registrable-suffix setups (e.g.--auth-webauthn-rp-id=example.comto share credentials across subdomains).forms.BoundField.Labelandforms.Choice.Labelare now auto-translated byextractFieldsvia the same Label-as-key convention asNavItem.Label. Templates render{{ .Label }}and get the locale-appropriate string for free — no{{ t .Label }}wrapping needed. Contribute translations keyed by the Englishverbose:/Choice.Labelvalue. AddedForm.WithContext(ctx)for building forms outside the request lifecycle (background jobs, CLI). See forms: Translating field labels.contrib/{admin,auth,jobs}short structured-key labels migrated to Label-as-key in line with the new convention. Filter tabs, action buttons, table headers, field labels, status badges — i.e. anything that's a single-word or short-phrase UI label — now use bare English keys ({{ t "Active" }}instead of{{ t "admin-users-active" }}). Full sentences, prompts, confirmations, and plural variants keep their structured keys. Translation TOMLs now have a "Field labels" block at the top followed by structured "Messages" — see i18n: Labels vs. Messages for the rule.- Den bumped to v0.14.0 —
den:tags now flow through nested struct fields. Profile-style extensions onauth.User[P](and any other named-struct embedder) can declareden:"index"/unique/fts/index_togetherdirectly on Profile fields; Den emits JSON-path indexes like$.profile.slugon both SQLite and PostgreSQL. See Den's Nested Field Indexes. Heads-up:den.Revertnow zeroes the doc before decoding the snapshot — burrow doesn't callRevert, so the behaviour change only matters for app code that does. - Den bumped to v0.15.0 — internal cleanup, no API change. Dropped
goccy/go-jsonandoklog/ulid/v2from the transitive graph; ULIDs are now produced by Den's in-tree monotonic generator (fixes a latent intra-millisecond ordering bug forSort("_id")and cursor pagination) and JSON encoding uses stdlib with a pooledEncoder.SetEscapeHTML(false)for JSONB columns. Wire-compatible. The removedstorage/s3backend was not used by any burrow contrib. burrow newscaffold template synced with Den 0.15. The embeddedgo.mod.tmplpinsoliverandrich/den v0.15.0and drops thegoccy/go-jsonandoklog/ulid/v2indirect entries; scaffolded projects build cleanly without a manual reconcile. Theinternal/app/app.go.tmplpackage doc loses the "Pattern B" jargon, matching the docs cleanup from PR #33.
Added¶
--app-nameglobal flag (APP_NAMEenv,server.app_nametoml) sets a human-readable application name available ascfg.Config.Server.AppName. Defaults to the binary basename (e.g.notes,myapp) so it's non-empty out of the box. Three consumers wired:- WebAuthn:
--auth-webauthn-rp-display-nameloses its"Web App"default and falls back to--app-namewhen not set explicitly. The browser passkey dialog now reads "Save passkey for Notes" instead of "Save passkey for Web App" by default. - SMTP From: a bare
--smtp-from=noreply@example.comis decorated as"Notes <noreply@example.com>"automatically. Pre-formatted--smtp-from="Acme <noreply@example.com>"is left alone. - Template func:
{{ appName }}returns the configured name. Use it in layout titles, email subjects, footer copy, etc.
- WebAuthn:
formspackage: nested-struct subform support. Struct-typed form fields now render as subforms —extractFieldsrecurses one level into the nested struct and exposes its fields under the parent'sBoundField.SubFields. NestedFormNamevalues follow theparent.childconvention (e.g.profile.name) soburrow.Binddecodes them and validation errors route to the correct nested field.time.Timeis excluded (keeps the"date"widget); pointer-to-struct is dereferenced (nil → zero-value sub-fields); recursion is capped at one level. Templates dispatch onType == "subform"and iterate.SubFields— see forms: Nested struct fields.- Translation guard test (
TestTranslationsLoadInGoI18n) loads every contrib'sactive.*.tomlthrough a real go-i18n bundle, so reserved-key collisions (id,hash,description,leftdelim,rightdelim, plural keys) surface at test time instead of at server boot.
Removed¶
cmd/burrow-tailwinddeprecation shim deleted. The standalone binary printed a deprecation notice and delegated toburrow tailwindin v0.21; v0.22 removes it. Existing projects: replace thetool github.com/oliverandrich/burrow/cmd/burrow-tailwinddirective ingo.modwithtool github.com/oliverandrich/burrow/cmd/burrow, and rewrite.mise.toml/.air.tomlinvocations fromgo tool burrow-tailwind ...togo tool burrow tailwind .... See the Tailwind guide.
Fixed¶
contrib/jobstranslation bundle no longer collides with go-i18n reserved keys. The Label-as-key migration had introducedID = "ID"inactive.{en,de}.toml;idis one of go-i18n's reserved top-level fields (alongsidehash,description, plural keys), so the loader rejected the file at boot. Renamed the label to "Job ID" in both TOMLs andtemplates/admin_detail.html.contrib/authadmin user-edit page renders Profile as a real subform.forms.extractFieldsalready emittedType: "subform"with populatedSubFieldsfor theUser[P].Profilestruct field, buttemplates/admin_user_form.htmlhad no subform branch — the field fell through to the text-input default and rendered as a literal{}placeholder. The template now extracts the per-field markup into a sharedauth/admin_user_fieldblock and adds a<fieldset>-based subform branch that recurses into.SubFields.contrib/authadmin user-edit Role dropdown matches the surrounding design. The native<select>previously kept the browser's default chevron and styling, visibly out of place next to the Tailwind-styled inputs. The select branch now wraps the element in agridcontainer, appliesappearance-none, reservespr-8for a custom inline chevron SVG positioned viajustify-self-end(pointer-events-noneso clicks still reach the select), and tints<option>backgrounds in dark mode via thedark:*:variant.
0.21.2 — 2026-05-18¶
Changed¶
- Dropped "Django-inspired" framing from current-state copy. Scaffolded project's homepage and README, plus the
formspackage doc, now describe Burrow on its own terms. The broader marketing comparison in burrow's own README anddocs/index.md("for Go developers who want something like Django, Rails, or Flask") is kept intentionally. - Scaffolded project's README rewritten for post-scaffold reality. Quick Start leads with
mise run setup && mise run devinstead of the obsoletegohatchinvocation; the gohatch requirement and the "Template Variables" metadata section are gone; the Development table usesburrow tailwind.
0.21.1 — 2026-05-18¶
Changed¶
burrow newbootstrap UX. The destination is now auto-initialized as a git repo whengitis onPATH(no initial commit — that's the user's choice). The printed Next-steps switches to a mise-aware two-liner (mise run setup+mise run dev) when mise is detected, falling back togo mod tidy && go runwhen it isn't. The scaffolded.mise.toml's[tasks.setup]is now a one-stop bootstrap:mise install+go mod tidy+ dev-key generation +pre-commit install(when.git/exists). Docs and README updated to lead with the mise path. No--no-gitflag — secondgit initis a harmless no-op for users in CI.
0.21.0 — 2026-05-18¶
Added¶
cmd/burrowCLI with three sub-commands:burrow new <dir> --module <path>scaffolds a new burrow project from an embedded template (the former go-burrow-template repo, now bundled);burrow generate app <name>scaffolds a contrib-style app stub inside an existing project, default path./internal/<name>;burrow tailwind <args...>consolidates the standalonecmd/burrow-tailwindtool. Install viago install github.com/oliverandrich/burrow/cmd/burrow@latest. Scaffolded projects auto-pin to the burrow version that generated them (viaruntime/debug.ReadBuildInfowith git-tag fallback).
Changed¶
- Docs reflect the new CLI. Quickstart and installation pages lead with
burrow new;docs/guide/tailwind.mdusesburrow tailwindthroughout (with a migration callout at the top); newdocs/reference/cli.mddocumentsnew,generate app, andtailwindsub-commands.
Deprecated¶
cmd/burrow-tailwindis deprecated in favour ofburrow tailwind. The standalone binary now prints a one-line deprecation notice and delegates to the new sub-command. Migrate by replacinggo tool burrow-tailwind ...withgo tool burrow tailwind ...in.mise.toml,.air.toml, and any other build scripts. Scheduled for removal in v0.22.
0.20.0 — 2026-05-17¶
Migrating from v0.18.x? See the v0.20 migration guide for the concrete before/after patterns.
Breaking Changes¶
- CSS stack swap: Tailwind v4 in, mucss / bootstrap / bsicons / alpine out. Burrow ships no CSS bundle of its own anymore — every in-tree contrib (
admin,auth,jobs) and example uses Tailwind v4 utilities. Apps consume them via the newcmd/burrow-tailwindwrapper, which auto-discovers every contrib's template directory so utility scanning Just Works. Theme-switcher machinery is gone; dark mode followsprefers-color-scheme. Apps still on the deleted contribs must either migrate to Tailwind (seeexample/notesanddocs/guide/tailwind.md) or vendor the old CSS themselves. NavItem.IconandNavLink.Iconare nowstring(a template-define name) instead oftemplate.HTML. Layouts render icons via the new{{ icon .Icon }}template function. Move icon SVGs intotemplates/icons.htmlwith{{ define "<app>/icon_<name>" }}blocks and setIcon: "<app>/icon_<name>".contrib/admin.DashboardItem.Iconfollows suit. Seedocs/guide/navigation.md.HasDocuments.Documents()returns[]document.Documentinstead of[]any. Den 0.13.1 tightenedden.Registerto take...document.Document; burrow's interface follows so the document-type contract is enforced at compile time end-to-end. Types embeddingdocument.Basesatisfy the marker automatically. Migration: replacereturn []any{&Foo{}, &Bar{}}withreturn []document.Document{&Foo{}, &Bar{}}and add thegithub.com/oliverandrich/den/documentimport.- Silent template-func stubs removed; contrib
Dependencies()reflect real coupling. The core base FuncMap no longer stubscsrfToken/csrfField/csrfHxHeaders/lang; templates using them without the providing contrib now fail at parse time instead of rendering empty.auth.Dependencies()is now[session, csrf, staticfiles];admin.Dependencies()addscsrfandauth;jobs.Dependencies()is now[admin]. Host apps registeringauth.New()withoutcsrforstaticfileswill fail at startup — register the missing contribs. auth.DefaultAuthLayout()returns"auth/layout"instead of"mucss/layout".contrib/authnow ships its own Tailwind-styled, navbar-less default layout. Hosts get a clean login page out of the box (no leaked host navbar). The layout links{{ staticURL "app/app.min.css" }}(Pattern B convention); hosts on a different CSS path override viaauth.WithAuthLayout("...")or pass""to inherit the host'ssrv.SetLayoutlayout.- Den bumped to v0.13.1 — top-level CRUD wrappers collapsed; struct-tag validation always-on.
den.Insert/den.Update→den.Save(branches ondoc.ID);den.InsertMany→den.SaveAll;den.DeleteMany[T](ctx, db, conds)→den.NewQuery[T](db, conds...).Delete(ctx);den.FindOneAndUpdate[T](...)→den.NewQuery[T](db, conds...).UpdateOne(ctx, fields).burrow.OpenDBWithoutValidationremoved (no escape hatch left to opt out of). See the Den changelog for the full table.
Added¶
- Tailwind v4 toolchain.
cmd/burrow-tailwindwraps the upstreamtailwindcssstandalone CLI, auto-generating@sourcedirectives for every contrib's template directory. Install viago get -tool github.com/oliverandrich/burrow/cmd/burrow-tailwind, thengo tool burrow-tailwind -i tailwind.css -o static/app.min.css [--watch | --minify]..mise.tomlpins both the Go toolchain andtailwindcsssomise installis the one-shot setup. New contribs are picked up automatically ongo get -u— no per-project source-list maintenance. Seedocs/guide/tailwind.md. {{ icon "name" }}template function in the core FuncMap. Looks up the named template define and returns its rendered HTML, used by layouts to renderNavItem.Icon/NavLink.Icon/admin.DashboardItem.Icondynamically. Necessary because Go's built-in{{ template "name" . }}action requires a string literal at parse time.Server.CLICommands()returns the same subcommands asServer.Registry().AllCLICommands()but wraps eachActionto open the DB and runConfigure()on every app first. Use this when wiring contrib subcommands likeauth promote— without it, the subcommand fires against uninitialised apps.Server.TemplateExecutor()exposes the framework's executor for non-HTTP rendering (background jobs, SSE broadcasts) after boot.burrowtest.StubApp(name)helper for tests that need to satisfy a contrib'sDependencies()declaration without actually exercising the depended-on contrib.
Changed¶
- Dev tooling migrated from
justfileto mise..mise.tomlpins the Go toolchain and dev tools (golangci-lint,tparse,goimports,govulncheck,go-licenses,go-ignore-cov,pre-commit); tasks live in[tasks.*]blocks ormise-tasks/<name>scripts.just <recipe>→mise run <task>. Runmise installafter cloning. contrib/jobsRescueStaleruns as a single bulk update instead of anAll+ per-rowUpdateOneloop, closing the race window between snapshot and rescue — thestatus=runningguard is now re-evaluated at write time. No public API change.- Documentation refreshed end-to-end for v0.20.
docs/guide/testing.mdrewritten against the currentburrowtestAPI (replaces removedburrow.TestDB/burrow.TestErrorExec*);docs/reference/server.mdboot sequence rewritten to matchServer.Runreality plus new sections forServer.CLICommands/Server.TemplateExecutor;docs/reference/core-functions.mdgains aburrow.OpenDBreference; type / flag / dep references aligned acrossinterfaces.md,core-functions.md,context-helpers.md,configuration.md,template-functions.md, and every contrib page. Tutorial parts 1-8 prose reconciled with the actualtutorial/stepNN/source;tutorial/step06..08/main.gogainCommands: srv.CLICommands().
Removed¶
contrib/mucss,contrib/bootstrap,contrib/bsicons,contrib/alpine— all four former design / icon / JS-helper contribs deleted alongside theAppConfig.RegisterIconFunc/IconFuncs/IconFuncAPI surface, the µCSS/Bootstrap CSS bundles, theupdate-icons/update-mucss/update-bootstrap/sassmise tasks, and the vendored Bootstrap-Icons + Alpine.js + theme-switcher assets.example/themes(a µCSS accent-variant preview) is also gone.
Fixed¶
admin/paginationtemplate lifted intocontrib/admin. The three near-identical{{ define "<app>/pagination" }}blocks (jobs/pagination,auth/pagination,notes/pagination) collapsed into a single canonicaladmin/paginationwith the same(dict "Page" .Page "BasePath" "..." "RawQuery" .RawQuery)contract. Downstream apps that overridejobs/pagination/auth/pagination/notes/paginationmust switch to overridingadmin/paginationinstead.tutorial/step03..08migrated off mucss. Theinternal/pagespackage was renamed tointernal/appand now ships a hand-writtenapp.cssviaHasStaticFilesinstead of depending on the deletedcontrib/mucss. Each step'smain.godrops themucss.New()registration; the layout templates link{{ staticURL "app/app.css" }}.- CLI subcommands now run with apps
Configure()'d.Server.CLICommands()(see Added) replacesServer.Registry().AllCLICommands()as the recommended wiring; without it,auth promoteand friends failed withauth app not initialized. Migration: in yourcli.Command{...}, replaceCommands: srv.Registry().AllCLICommands()withCommands: srv.CLICommands(). {{ icon "..." }}no longer poisons the global template tree.Servernow keeps a separateiconTemplatesClone for icon execution so executing icon defines doesn't mark the maintemplatestree as "executed". Before the fix, any page that rendered an icon followed by a layout wrap returnedHTTP 500: html/template: cannot Clone "" after it has executed. Regression test:TestExecuteTemplateUsingIconDoesNotPoisonClone.example/notesno longer renders the empty-state placeholder alongside created notes.notes/create_responseOOB-deletes the#notes-emptyplaceholder when prepending the first note, and a newnotes/delete_responsere-injects it when the last note is deleted.
0.18.1 — 2026-05-06¶
Fixed¶
- Vendored Alpine.js and µCSS attributions added to
THIRD_PARTY_LICENSES.md. Alpine.js v3.15.8 (MIT, Copyright (c) 2019-2025 Caleb Porzio and contributors) undercontrib/alpine/static/and µCSS v1.4.8 (MIT, Copyright (c) 2026 Digicreon) undercontrib/mucss/static/were both shipped without an entry in the third-party listing.
0.18.0 — 2026-05-05¶
Breaking Changes¶
contrib/adminported to µCSS, sidebar dropped. Admin uses a top-nav layout with a card-grid dashboard at/admin/; per-page breadcrumbs replace the sidebar, and the admin-area templates incontrib/auth,contrib/jobs, andexample/noteswere rewritten in µCSS. Public-API:SidebarGroup/SidebarItem/PrepareSidebar→DashboardGroup/DashboardItem/PrepareDashboard; theActivefield,sidebarLinkClass,isActivePath, and theadminSidebartemplate func are gone (nowadminDashboard).contrib/adminnow declaresstaticfiles,htmx,mucss,messagesas Dependencies. Apps with custom admin extensions emitting Bootstrap markup must port to µCSS.contrib/authuser-facing templates ported to µCSS.auth.DefaultAuthLayout()now returns"mucss/layout"; element IDs unchanged sowebauthn.jskeeps working. Inline JS visibility toggles switched fromclassList.add/remove("d-none")to nativeelement.hidden = true/false— custom auth templates copying that pattern must follow. Apps still on Bootstrap must setauth.New(auth.WithAuthLayout("bootstrap/layout"), auth.WithRenderer(...)).
Added¶
contrib/mucss— new default design contrib providing µCSS v1.x (PicoCSS v2 derivative with upstream bugs fixed and richer components: Hero, Alert, Toast, Modal, Pagination, Badge, Tabs). 20 named accent variants (WithColor),WithCompactType()for app/admin UIs, customisable via CSS custom properties (WithCustomCSS). The app implementsConfigurableto registericonSunFill,iconMoonStarsFill, andiconCircleHalftemplate functions used by the theme switcher.contrib/mucssships Burrow UX extras (mu-extras.min.css, always loaded unlessWithCustomCSSis set): navbar-dropdown min-width, navbar-icon vertical centering, dialog header flex layout, dialog form-footer button layout, and a.field-errorrule for inline form errors.- HTMX-driven dialog pattern.
htmx.OpenDialog(w, id [, class])andhtmx.CloseDialog(w, id)emit events that thehtmx/dialog_scripttemplate turns intodialog.showModal()/close()calls. Themucssandbootstrapnav layouts ship a permanent<dialog id="modal">container and include the script. The optional class arg switches the µCSS modal size variant. Inside an open dialog, clicking any element withrel="prev"(µCSS close-icon button convention) or the behavioraldata-close-dialogattribute closes the dialog client-side without a round-trip. Seeexample/notesfor the full create + edit flow. example/themes— new demo app (just example-themes) previewing every µCSS accent variant side-by-side with the canonical button/card/alert/badge layout. Useful for picking aWithColor(...)value for your project.uploader.ServeHandlersupports Range and conditional GET when the underlying Storage implementsden.SeekableStorage(file backend). Browsers can now stream video Range-by-Range, and clients with cached copies get 304 Not Modified instead of a full re-download. ETag is the storage path (content-addressed, so stable). Non-seekable backends (S3) keep the existingio.Copyfallback — Range support there belongs at the URL layer (pre-signed URLs).burrow.CacheControlImmutableconstant for the year-long immutable Cache-Control value used by content-addressed responses.contrib/staticfilesanduploaderboth consume it; downstream apps that serve content-addressed bytes can use it too instead of inlining the literal.
Changed¶
- Den bumped to v0.11.2.
OpenDBandOpenDBWithoutValidationnow wrap the typedden.ErrUnsupportedSchemesentinel, so callers can match unsupported-scheme errors viaerrors.Isinstead of substring-matching the message.uploader.ServeHandlerconsumes the newden.SeekableStoragecapability for Range / conditional-GET support (see Added). contrib/mucssupdated to µCSS 1.4.8. Upstream fix for<details class="dropdown">summaries inside coloured navbars — the same pattern Burrow's theme switcher uses. No API or token change.example/helloported to µCSS withWithCompactType, a.heroblock, and a native<dialog>demo. Theexample/hello-picoevaluation example was removed.example/notesuser-facing pages ported to µCSS.bootstrap.New()stays alongsidemucss.New(WithCompactType())until the admin-area auth templates migrate.- Docs lead with µCSS as the default design contrib. README, getting-started, contrib/admin, tutorial part 6, and reference docs updated; the "CSS framework default" Open Question is resolved (µCSS default, Bootstrap deprecated).
- Tutorial Part 8: Custom Admin Views. New
tutorial/step08extends the polls app withburrow.HasAdminand full Question/Choice CRUD admin views; newdocs/tutorial/part8.mdwalks through it. Index, mkdocs nav, and Part 7's "Next" pointer updated. tutorial/step03–step07ported to µCSS. Each step registersmucss.New(). The user menu evolves: step05/06 use a simple inline sign-out form; step07 — once htmx is loaded — refactors into a<details class="dropdown">withhx-post. Polls app gainsSeedable(Seed(ctx)for--seed). Tutorial docspart3.md–part7.mdupdated to match.
Deprecated¶
contrib/bootstrapis deprecated, removal in v0.20. Critical fixes only. New projects should usecontrib/mucss— the port is mostly mechanical (<div class="card">→<article>,alert-danger→alert-error).contrib/bsicons(not Bootstrap-coupled) stays.
Fixed¶
mucss/theme_switchermarks the currently active option witharia-current="true"(rendered bold viamu-extras.min.css). Previously all three options looked identical when the dropdown was open.
0.17.0 — 2026-04-28¶
Breaking Changes¶
-
Den backend imports moved out of
db.go— every binary usingburrow.OpenDBmust now blank-import the Den backend that matches its DSN in its ownmain.go. Burrow no longer pulls in either backend by default, so production binaries only link the engine they actually use (sqlite-only deployments drop ~3 MB; postgres-only deployments drop ~9 MB). Migration:// main.go import ( _ "github.com/oliverandrich/den/backend/sqlite" // for sqlite:// DSNs _ "github.com/oliverandrich/den/backend/postgres" // for postgres:// DSNs )OpenDBandOpenDBWithoutValidationwrap Den's "unsupported database scheme" error with the exact import path to add. Theden/storage/fileblank-import stays indb.go(negligible weight, no native deps);s3://is already opt-in viaden/storage/s3. -
burrow.TestDB,burrow.TestErrorExecContext,burrow.TestErrorExecMiddlewaremoved to new sub-packageburrow/burrowtestand renamed (Test prefix dropped, matching thehttptest/fstestconvention). Migration:// before db := burrow.TestDB(t) ctx := burrow.TestErrorExecContext(ctx) handler := burrow.TestErrorExecMiddleware(next) // after import "github.com/oliverandrich/burrow/burrowtest" db := burrowtest.DB(t) ctx := burrowtest.ErrorExecContext(ctx) handler := burrowtest.ErrorExecMiddleware(next)The sub-package blank-imports SQLite for
DB(t). Production binaries that do not depend onburrowtesttherefore do not link SQLite via this path.
0.16.0 — 2026-04-28¶
Breaking Changes¶
--media-url-prefixflag removed — the public URL prefix for locally served attachments now lives inside--storage-dsnas a?url_prefix=…query parameter. Default DSN isfile:///data/media?url_prefix=/media/, so out-of-the-box behavior is unchanged. Migrate explicit configs by folding the prefix into the DSN, for exampleSTORAGE_DSN='file:///data/media?url_prefix=/uploads/'. TheMEDIA_URL_PREFIXenv var, thestorage.url_prefixTOML key, and theStorageConfig.URLPrefixGo field are gone with the flag.- Den upgraded to v0.11.0 — see the den v0.11.0 CHANGELOG for the full list. The two changes that surface in burrow's contract: the single-arg
storage.OpenURL(covered above) and the[]where.Conditionslice argument onFindOneAndUpdate(callers that pass conditions to it must wrap them in a slice).
0.15.0 — 2026-04-19¶
Breaking Changes¶
contrib/uploads→uploader— moved out ofcontrib/because it is no longer aburrow.App. New import path:github.com/oliverandrich/burrow/uploader. Construct the*uploader.Uploaderin your domain app'sConfigure(u := uploader.NewUploader(cfg.DB)) and register the serving route inRoutes(uploader.Mount(r, cfg.DB.Storage())). File ingress isu.Store(r, "file", opts). Gone:App,Storeinterface,LocalStorage, allWith*options, all--uploads-*flags andUPLOADS_*env vars, the middleware, the context helpers,StoreFile,ErrNoStorage,StoreOptions.Storage, andStoreOptions.Prefix.NewUploaderpanics if the DB has no Storage. Orphan-bytes cleanup onInsert/Updatefailure is the caller's responsibility (_ = u.Storage().Delete(ctx, att)); an offline sweeper is planned. Seedocs/guide/uploader.mdfor the new shape.
Added¶
-
Built-in Storage configuration — two new core flags construct a
den.Storageat boot and install it on the opened*den.DB:--storage-dsn(STORAGE_DSN, defaultfile:///data/media) — Storage URL. Schemes:file://(SQLAlchemy/JDBC-style:file:///relativeorfile:////absolute; one leading slash is stripped on parse). Set to an empty string to disable Storage.--media-url-prefix(MEDIA_URL_PREFIX, default/media/) — public URL prefix for locally served attachments.
Domain apps receive the configured Storage via
cfg.DB.Storage(); nomain.gosetup is required. Mirrors the--database-dsnpattern so the full data layer is flag-driven. - Built-inmediaURLtemplate function — when a Storage is installed on the DB, Burrow auto-registersmediaURL(an alias forden.Storage.URL) in the globalFuncMap. Templates can render attachments with{{ mediaURL .Hero }}without any per-appFuncMapwiring. When Storage is disabled, the function is not registered, so any reference fails at template-parse time — the right signal for an unwired dependency. -burrow.OpenDBacceptsden.Optionvariadics —OpenDB(ctx, dsn, opts...)forwards options toden.OpenURLafter applyingvalidate.WithValidation(). Used internally to layer inden.WithStorage; callers who open the DB themselves (tests, tools) can layer in their own options. - Tooling page in Getting Started docs — newgetting-started/tooling.mddocuments the two companion projects: thego-burrow-templateproject template (scaffolds a runnable Burrow app with contrib stack, air live reload, goreleaser config, and CI) and theburrow-claude-pluginClaude Code plugin (specialized agents and commands for feature development, architecture, and review). Linked fromquickstart.mdas a "faster start" entry point. -zizmorworkflow audit in CI — newzizmorjob in.github/workflows/ci.ymlruns the zizmor static analyzer against all workflow files on every PR and push. A.github/zizmor.ymlconfig documents the one accepted risk (theworkflow_runtrigger inrelease.yml, gated on branch-prefix + CI success).
Changed¶
- Default
--database-dsnis nowsqlite:///data/app.db— colocated with the attachment storage root at./data/media, so a fresh project keeps all its local state in one directory. Den v0.10.1's SQLite backend auto-creates the parent directory onOpen, so first-run setup needs no manualmkdir. - Den upgraded to v0.10.1 — picks up the new
document.Attachmentembed,den.Storageinterface, pluggable storage-backend registry (storage.OpenURL+storage/filesub-package), theURLPrefix()accessor theuploaderpackage uses to derive its serving route, and SQLite auto-mkdir on open. - CI and release workflows hardened — all
actions/*andgolangci/*uses are now pinned to commit SHAs with version comments.persist-credentials: falseon everyactions/checkout.actions/setup-goruns withcache: falseto prevent cache-poisoning on tag pushes.release.ymlroutesgithub.event.workflow_run.head_branchthrough aVERSIONenv var instead of direct${{ … }}interpolation insiderun:blocks, closing the template-injection vector. The manualactions/cachesteps were removed.
0.14.0 — 2026-04-19¶
Breaking Changes¶
- Den upgraded to v0.8.0 — picks up the sealed
Scopeunification (Tx* CRUD variants removed), ctx-on-terminals forQuerySet, ctx onOpen/OpenURL, renamed change-trackingRollback→Revert, composabledocument.SoftDelete/document.Trackedembeds (replacingSoftBase/TrackedBase/TrackedSoftBase), non-blocking PostgreSQL index creation, GIN-friendlyEqpredicates, and batchedWithFetchLinks. See the den v0.8.0 CHANGELOG for the full list. burrow.OpenDBandburrow.OpenDBWithoutValidationtake a leadingcontext.Context— mirroring den's newOpen/OpenURLsignature. A cancelled context aborts the open cleanly; callers with a startup deadline now get the guarantee they expect. Migration:burrow.OpenDB(dsn)→burrow.OpenDB(ctx, dsn). The internal server boot already hasctxin scope; tests should passt.Context().
Changed¶
contrib/jobsclaim usesSELECT ... FOR UPDATE SKIP LOCKED— the per-candidate CAS retry loop is replaced by a single locking query inside a transaction. N concurrent workers each receive a disjoint slice of the pending set without blocking each other, removing the O(N²) CAS contention under load. On SQLite theForUpdatemodifier is a no-op (IMMEDIATE transactions already serialize writers), so behavior is unchanged there. No schema migration required. Enabled by den v0.8.0'sden.NewQuery[T](tx).ForUpdate(den.SkipLocked()).
0.13.1 — 2026-04-15¶
Changed¶
- Den upgraded to v0.7.0 — picks up PostgreSQL JSONB comparison fixes, GroupBy SQL pushdown, revision TOCTOU fix, and link validation enforcement
Fixed¶
- Invalid den struct tags in test models —
den:"name,index"andden:"key,unique"incorrectly included field names in the den tag (field names come from the json tag). Den v0.7.0 now rejects unknown tag options, catching these at registration time
0.13.0 — 2026-04-15¶
Breaking Changes¶
- Auth admin rewritten with hand-written handlers —
contrib/authno longer uses ModelAdmin for its admin views. User list, edit, delete, and invite management are now direct handlers with custom templates. TheHasAdmininterface (AdminRoutes,AdminNavItems) is unchanged. Auth admin templates now use globally registered icon functions (iconSearch,iconPlus,iconPersonSlash,iconPersonCheck,iconTrash,iconXCircle). - Jobs admin rewritten with hand-written handlers —
contrib/jobsno longer uses ModelAdmin. Job list, detail, delete, retry, and cancel are now direct handlers with custom templates including status filter pills and inline action buttons. - ModelAdmin package removed —
contrib/admin/modeladminhas been deleted entirely. All admin views now use hand-written handlers. The admin coordinator (layout, sidebar, auth middleware, nav groups) is unchanged.
Added¶
- User search in admin — the user admin list now supports search across username, name, and email fields.
- Inline invite creation — the invite admin list now features an htmx-powered inline form that slides open on button click instead of navigating to a separate page.
burrow.DefineTask[P]()— type-safe generic task definitions for the jobs system. WrapsQueue.HandleandQueue.Enqueuewith automatic JSON marshalling, ensuring compile-time agreement between producer and consumer payload types. Auth email jobs migrated as first consumer.burrow.DefineResultTask[P, R]()— variant ofDefineTaskfor handlers that return both a result and an error. Results are persisted as JSON on the job and visible in the admin detail view.- Job result persistence — completed jobs now store their handler's return value in a
Resultfield (JSON). Failed jobs additionally record the Go error type inErrorClassand the timestamp of the last handler invocation inLastAttemptedAt. All three fields are displayed in the admin detail view and cleared on retry. - Job priority — jobs now have a
Priorityfield (default 0, higher = more urgent). Set per-type viaburrow.WithPriority(n)at handler registration. The claim query picks highest-priority jobs first, with FIFO ordering within the same priority level.
Changed¶
- Seed requires
--seedflag —Seedable.Seed()no longer runs unconditionally on every server start. Pass--seed(or setSEED=true) to run seed functions. This prevents non-idempotent seeders from creating duplicates on restart. - Auth handlers split into focused files — the monolithic
handlers.go(714 lines, 25 functions) has been split intohandlers_registration.go,handlers_login.go,handlers_credentials.go,handlers_recovery.go,handlers_email.go, andhandlers_helpers.go. No API or behavior changes — pure file reorganization for better navigability. - Admin decoupled from auth package —
contrib/adminno longer importscontrib/authdirectly. Instead, it discovers auth middleware via the newburrow.AdminAuthinterface from the registry.contrib/authimplementsAdminAuthautomatically. Custom auth systems can provide their own implementation. Config.IsHTTPS()helper — replaces duplicatedstrings.HasPrefix(baseURL, "https://")checks in csrf, session, and secure apps.- WebAuthn flag aliases removed — the legacy
--webauthn-rp-id,--webauthn-rp-display-name,--webauthn-rp-originaliases (withoutauth-prefix) have been removed. Use the canonical--auth-webauthn-*names. burrow.Queuesplit intoEnqueuer+Queue— newEnqueuerinterface holdsEnqueue,EnqueueAt, andDequeue.QueueembedsEnqueuerand addsHandle. Code that only submits jobs can now acceptEnqueuerinstead of the fullQueue.TaskDefinitionandResultTaskstoreEnqueuerinternally.- Form fields with nil pointers render as zero values —
forms.extractFieldsnow returns the element type's zero value (e.g.""for*string) instead ofnilwhen a pointer field is nil. Templates can use{{ .Value }}on optional fields without special-casing nil.
Fixed¶
- Job retry backoff capped at 1 hour — exponential backoff (
baseDelay * 2^(attempts-1)) now caps at 1 hour. Previously, high attempt counts could overflowtime.Durationand produce negative or astronomically large delays. - Session flush logs encoding errors —
state.flush()now logs viaslog.Errorwhen cookie encoding fails instead of silently swallowing the error. - Admin rejects duplicate AdminAuth providers —
admin.Configure()returns an error if multiple apps implementAdminAuth, instead of silently using the first one. - Session deferredWriter supports http.Flusher — the session middleware's response wrapper now implements
Flush(), fixing SSE and streaming handlers that use directw.(http.Flusher)type assertions. - Session cookies written once per request —
session.Set(),Delete(), andSave()no longer write theSet-Cookieheader immediately. Instead, the session middleware defers the write until the response is sent, producing exactly oneSet-Cookieheader regardless of how many session mutations occur. Previously, eachSet()call wrote a separate header, and only the last one survived to the browser. - Jobs recover from handler panics — worker goroutines now recover from panics in job handlers, converting them into failures with a stack trace. The worker stays alive and continues processing other jobs.
- RenderError falls back to plaintext — when both
error/{code}anderror/defaulttemplates are missing,RenderErrornow writes a plaintext HTTP error instead of a blank response. - NavItem.LabelKey now translated in navLinks —
buildNavLinksnow translatesLabelKeyviai18n.Tat render time, falling back toLabelwhen no translation is found. PreviouslyLabelKeywas silently dropped. - Uploads no longer buffer entire file in memory —
LocalStorage.Storenow streams uploads directly to a temp file while computing the SHA-256 hash, instead of reading the entire file into[]byte. Only the first 512 bytes are buffered for MIME detection. MIME validation happens before reading the body, so rejected files are discarded early. - Jobs admin UI re-enabled —
contrib/jobsAdminRoutesandAdminNavItemswere stubbed out during the Den migration. The ModelAdmin integration is now wired up again, restoring the list/detail/retry/cancel/delete views and the sidebar nav entry. - Auth redirects use SmartRedirect —
Logout,RecoveryCodesPage, andAcknowledgeRecoveryCodesnow usehtmx.SmartRedirectinstead ofhttp.Redirect, fixing redirect behavior when triggered via htmx. - Invite creation uses SmartRedirect —
handleCreateInvitenow useshtmx.SmartRedirectinstead ofhttp.Redirect, fixing redirect behavior when the form is submitted via htmx.
0.12.0 — 2026-04-08¶
Breaking Changes¶
- Den struct-tag validation is now enabled by default —
burrow.OpenDB()now enables Den'svalidate.WithValidation()automatically, so any document field tagged withvalidate:"..."(e.g.,validate:"required",validate:"email",validate:"oneof=...") is enforced before every Insert and Update. Violations return an error wrappingden.ErrValidation.
Projects that had validate: tags on Den documents where the tags previously had no effect will now see those constraints enforced. If the data layer needs to stay lax temporarily during migration, use the new burrow.OpenDBWithoutValidation() escape hatch. Remove it once the data is clean.
burrow.TestDB() and authtest.NewDB() also enable validation so test code runs with the same constraints as production.
- Upgraded to Den v0.6.0 — Den now runs mutating hooks (
BeforeInsert,BeforeUpdate,BeforeSave) before both struct-tag validation and theValidator.Validate()interface. This lets aBeforeInserthook populate a default value for a field that validation then requires, matching the ActiveRecord/Django/SQLAlchemy pattern. See the Den changelog for details. If you had custom validation that depended on running before the hooks (unusual), move it intoBeforeInsertitself.
Added¶
burrow.OpenDBWithoutValidation(dsn)— opens a database with struct-tag validation disabled. Intended only as a migration escape hatch when moving a project from pre-v0.12.0 behavior.
0.11.4 — 2026-04-06¶
Changed¶
- Den updated to v0.5.0 — adds support for composite indexes via
index_togetherandunique_togetherstruct tags, and fixesSettings.Indexesapplication duringRegister() - Composite indexes for Job model — added
index_together:claim(RunAt, Status, WorkerID) andindex_together:stale(Status, LockedAt) to optimize worker claim and stale-job-rescue queries - Composite index for RecoveryCode model — added
index_together:recovery_status(UserID, Used) to optimize unused recovery code lookups
0.11.3 — 2026-04-06¶
Changed¶
- Den updated to v0.4.2 — adds PostgreSQL version check (requires PostgreSQL 13+) and LLM documentation
0.11.2 — 2026-04-05¶
Changed¶
- License page — include full MIT license text, link to third-party licenses
- Release workflow — release now waits for CI success before creating a GitHub release
0.11.1 — 2026-04-05¶
Fixed¶
- Social card URLs — updated
og:imageandtwitter:imagemeta tags to point to readthedocs.io
0.11.0 — 2026-04-05¶
Breaking Changes¶
- Database layer replaced: Bun → Den — the entire database layer has been replaced with Den, an object-document mapper (ODM) for Go. Same API for SQLite and PostgreSQL via URL-based DSN.
- All IDs changed from
int64tostring— documents now use ULID-based string IDs viadocument.Base Migratableinterface replaced byHasDocuments— apps declare document types instead of providing SQL migration files. Den creates tables and indexes automatically from struct tags.- DSN requires URL scheme —
--database-dsn sqlite:///app.db(default) or--database-dsn postgres://host/db. Plain file paths no longer accepted. --jobs-databaserenamed to--jobs-database-dsn— env varJOBS_DATABASE_DSN, TOML keyjobs.database_dsn- SQL migration files removed — schema is managed automatically from document struct definitions
- License changed from EUPL-1.2 to MIT
Added¶
- PostgreSQL support — switch between SQLite and PostgreSQL with a single flag, same code, same API
- New contrib app:
humanize— i18n-aware template functions for human-friendly display of times, numbers, and file sizes, inspired by Django'sdjango.contrib.humanize
Changed¶
- Handler pattern simplified — handlers are now methods on
*Appinstead of a separateHandlersstruct - Bun dependency removed — replaced by
github.com/oliverandrich/den v0.4.0 - Job queue: ownership guard — workers stamp
worker_idon claimed jobs;Complete/Failverify ownership via guardedFindOneAndUpdate, preventing double processing under concurrent workers - Job queue:
Complete/Failtake*Job— no longer reload from DB, eliminating redundant reads and stale attempt counts
Fixed¶
- Admin HTMX navigation — boosted requests with a custom
hx-target(e.g.#main) no longer wrap content in the layout, fixing the doubled sidebar itoatemplate function removed — was a no-op after int64→string migration; templates use.IDdirectly- Dead code removed —
URLParamInt64,MustURLParamInt64, stalebun:tags in tests
0.10.0 — 2026-03-29¶
Breaking Changes¶
- App interface simplified:
Appnow only requiresName() string—Register(cfg *AppConfig) errorhas been removed. All setup logic moves intoConfigure(cfg *AppConfig, cmd *cli.Command) errorvia theConfigurableinterface. - Configurable signature changed:
Configure(cmd *cli.Command) error→Configure(cfg *AppConfig, cmd *cli.Command) error— update all implementations to accept the newcfgparameter - PostConfigurable signature changed:
PostConfigure(cmd *cli.Command) error→PostConfigure(cfg *AppConfig, cmd *cli.Command) error - HasFlags extracted:
Flags()is no longer part ofConfigurable— it is now a standaloneHasFlagsinterface - Registry.RegisterAll removed: Use
Registry.ConfigureAll(cfg *AppConfig)instead (callsConfigure(cfg, nil)on eachConfigurableapp)
0.9.0 — 2026-03-28¶
Breaking Changes¶
- HasRequestFuncMap: Changed signature from
RequestFuncMap(*http.Request)toRequestFuncMap(context.Context)— update all implementations by replacingr *http.Requestwithctx context.Contextandr.Context()withctx(#5) - TemplateExecutor: Changed signature from
func(*http.Request, string, map[string]any)tofunc(context.Context, string, map[string]any)(#5)
Added¶
Startablelifecycle interface — counterpart toHasShutdown, called after the full boot sequence (templates built, middleware and routes registered); receives*Serverso apps can access server resources likeTemplateExecutor()(#11)- jobs: automatic
TemplateExecutorinjection — the jobs app now implementsStartableand injects theTemplateExecutorinto every job handler's context, enablingRenderFragmentin background jobs without manual setup (#11) - Added
RenderFragment()for rendering templates outside HTTP handlers (background jobs, SSE, CLI) (#5) - Added
Server.TemplateExecutor()accessor to obtain the template executor after boot (#5) - Added
WithRequestPath()/RequestPath()context helpers for request path propagation (#5) - htmx: Added smart response helpers
SmartRedirectandRenderOrRedirectthat handle htmx/non-htmx branching, plusReselectheader setter andStatusStopPollingconstant (#9) - Added
URLParamInt64()andMustURLParamInt64()helpers for parsing numeric URL parameters (#8) - auth: Added
MustCurrentUser()helper that returns the authenticated user or panics — for use behindRequireAuthmiddleware (#7) - sse: Added
BrokerFromRegistry()for package-level access to the SSE broker without type assertions (#6) csrfHxHeaderstemplate function — rendershx-headers='{"X-CSRF-Token":"..."}'as an HTML attribute when the csrf app is registered, or nothing when it is not. Use<body {{ csrfHxHeaders }}>for automatic CSRF protection on all htmx requests.csrfToken/csrfField/csrfHxHeadersfallbacks — these template functions are always available (return empty values when the csrf app is not registered), so layouts can reference them unconditionally.
Fixed¶
- Bootstrap/admin layouts: automatic CSRF token for htmx —
bootstrap/layout,bootstrap/nav_layout, andadmin/layoutnow use{{ csrfHxHeaders }}on the<body>tag, so all htmx requests include the CSRF token automatically.
0.8.0 — 2026-03-22¶
Added¶
- htmx: bundle SSE extension —
htmx/jstemplate now includes the htmx SSE extension (htmx-ext-ssev2.2.4) alongside htmx itself
0.7.4 — 2026-03-22¶
Added¶
PostConfigurableinterface — new optional app interface for second-pass configuration after allConfigure()calls complete; used bycontrib/jobsto guarantee thatRegisterJobs()runs after all apps are fully configured
Fixed¶
- Jobs:
RegisterJobstiming —HasJobs.RegisterJobs()is now called duringPostConfigure()instead ofConfigure(), ensuring that apps can safely access state set in their ownConfigure()when registering job handlers (fixes #4)
0.7.3 — 2026-03-22¶
Changed¶
burrow.TestDBuses file-backed SQLite —TestDB(t)now creates the database int.TempDir()instead of usingfile::memory:, preventing data loss when the connection pool recycles connections- Removed
internal/sqlitetest— all internal test code now uses the publicburrow.TestDB(t)helper
0.7.2 — 2026-03-21¶
Fixed¶
- Jobs: flaky test fix —
sqlitetest.OpenDBnow uses a file-backed SQLite database int.TempDir()instead offile::memory:, preventing data loss when the connection pool recycles connections
0.7.1 — 2026-03-21¶
Security¶
- Uploads: path traversal protection —
LocalStorage.Open,Delete, andPathnow validate that the resolved path stays within the root directory; returnsErrPathTraversalfor keys containing..sequences - Auth: timing-safe recovery login —
RecoveryLoginnow runs a dummy bcrypt comparison when the username is not found, preventing timing-based username enumeration - Auth: atomic registration — removed TOCTOU race in
RegisterBeginby eliminating pre-insert existence checks; UNIQUE constraint violations now return a clean "registration failed" instead of 500 - Auth: WebAuthn session store cap — in-memory session store is now capped at 10,000 entries to prevent denial-of-service via unauthenticated endpoints
- Jobs: atomic Retry/Cancel — status checks moved into the UPDATE WHERE clause to prevent a race where a running job could be reset to pending and executed twice
Fixed¶
- RenderError: no more empty bodies — added
error/defaultfallback template and specific templates for 401, 422, 429 in both core and bootstrap;RenderErrornow falls back toerror/defaultwhen the code-specific template is missing journal_size_limitPRAGMA per-connection — moved from one-shotdb.ExectowithPerConnPragmasso it is applied to every new pool connection, not just the first- Signal registration cleanup —
signalDonenow callssignal.Stopto clean up OS signal registration after the first signal is received - Uploads: extension from MIME type — storage keys now derive the file extension from the detected MIME type instead of the attacker-controlled filename, preventing malicious extensions (e.g., uploading a JPEG as
evil.php) - Auth: admin promotion uses CountAdminUsers — first-user admin promotion now checks
CountAdminUsers == 0instead ofCountUsers == 1, avoiding interference from phantom users created by abandoned registration flows - Ratelimit: trust-proxy security warning — added prominent documentation warning that
--ratelimit-trust-proxymust only be enabled behind a reverse proxy that overwritesX-Real-IP
Fixed¶
- Dead code removed — unused
dbfield onRegistrystruct removed - Signal cleanup —
signalDonenow callssignal.Stopafter receiving the first signal
0.7.0 — 2026-03-21¶
Breaking Changes¶
- Uploads: flag names renamed —
--upload-dir→--uploads-dir,--upload-url-prefix→--uploads-url-prefix,--upload-allowed-types→--uploads-allowed-types. Environment variables changed accordingly (UPLOAD_*→UPLOADS_*). This aligns with the{appname}-{property}convention used by all other apps. - Uploads:
Storageinterface renamed toStore— the context getter is nowuploads.Storage(ctx)returninguploads.Store. The oldGetStorageandStorageFromContextremain as deprecated wrappers. - Context getter renames —
UserFromContext→CurrentUser,LogoFromContext→Logo,NavGroupsFromContext→NavGroups,RequestPathFromContext→RequestPath. Old names remain as deprecated//go:fix inlinewrappers. - Auth: flag prefix corrected —
webauthn-rp-id→auth-webauthn-rp-id,webauthn-rp-display-name→auth-webauthn-rp-display-name,webauthn-rp-origin→auth-webauthn-rp-origin. Old names kept as CLI aliases. - Auth: Renderer method renames —
VerifyEmailSuccess→VerifyEmailSuccessPage,VerifyEmailError→VerifyEmailErrorPage
Added¶
- SSE contrib app — new
sseapp providing an in-memory pub/sub broker for Server-Sent Events; supports static and dynamic topics, non-blocking publish, configurable buffer size, 30s keepalive, graceful shutdown, and seamless htmx SSE extension integration viaContextHandler - Alpine.js contrib app — new
alpineapp that embeds Alpine.js 3.15.8 and serves it viastaticfileswith content-hashed URLs; include via{{ template "alpine/js" . }}in layout templates - Jobs: separate database support — new
--jobs-databaseflag to store the job queue in a dedicated SQLite file, eliminating write contention with the main application database burrow.OpenDB()— exported function to open SQLite databases with the framework's standard PRAGMAs; useful for contrib apps or user code that need dedicated database connectionsburrow.RenderContent()— renders pre-rendered HTML with layout wrapping and HTMX support; used by modeladmin, replaces duplicatedrenderWithLayoutlogic- Benchmarks — 20 benchmarks for critical code paths (Handle, Render, JSON, Bind, Pagination, Template Clone)
internal/sqlitetest— shared test helper for consistent in-memory SQLite test databases
Changed¶
- Auth repository: explicit ErrNotFound — all Get* methods now explicitly check
sql.ErrNoRowsand returnErrNotFounddirectly, matching the jobs repository pattern - Deployment guide improvements — hardened systemd unit (dedicated user, RuntimeDirectory, EnvironmentFile), added SQLite production section (file permissions, WAL sidecar files, backup strategies)
- Error handling consistency — fixed routing.md to correctly describe
RenderErrorbehavior; addedValidationErrordocumentation to error handling guide - Comprehensive documentation improvements — quickstart clarifications, middleware authoring example, template error behavior, navLinks explanation, migration rollback strategy, TOML config path, ACME rate limits, logging configuration, GoReleaser Homebrew Cask guidance
- Building Releases guide — new documentation page covering local builds, cross-compilation with GoReleaser, automated GitHub releases, version injection, and CI workflows
- Multi-Tenant guide — new documentation page explaining database-per-user architecture with
burrow.OpenDB()
Fixed¶
- Empty context.go files removed — package doc comments moved from
secure/context.goandjobs/context.goto their respectiveapp.gofiles
0.6.0 — 2026-03-21¶
Breaking Changes¶
- ModelAdmin:
Renderer.ConfirmDeletesignature changed —ConfirmDelete(w, r, item *T, cfg)is nowConfirmDelete(w, r, ids []string, impacts []CascadeImpact, cfg). Custom renderers must update their implementation. - ModelAdmin:
DeleteImpactsremoved fromRenderConfig— cascade impacts are now passed directly toConfirmDeleteinstead of being embedded inRenderConfig. - ModelAdmin: delete routes changed —
GET /{id}/deleteandDELETE /{id}replaced by unifiedPOST /bulk/delete(confirm page) andDELETE /bulk/delete(execute deletion). Both accept_selectedform parameter with one or more IDs.
Added¶
- ModelAdmin:
ConfirmPageonBulkAction— bulk actions withConfirmPage: trueredirect to a confirm page instead of using a JSconfirm()dialog; the built-inDeleteBulkActionuses this by default - Admin layout:
hx-booston<body>— replaces manualhx-get/hx-target/hx-push-urlon every link; all navigation and form submissions are automatically boosted to swap<main>without full page reloads AppConfig.RegisterIconFunc()— apps register icon template functions in theirRegister()method viacfg.RegisterIconFunc("iconName", bsicons.IconFunc); duplicate registrations are silently ignored, allowing multiple apps to depend on the same icon without collisions- Bootstrap color themes — three Sass-compiled color themes (blue, purple, gray) selectable via
bootstrap.WithColor(); default is purple; vanilla Bootstrap available viabootstrap.WithColor(bootstrap.Default) bootstrap.NavLayout()— layout template with emptybootstrap/navbar,bootstrap/alerts, andbootstrap/nav_scriptsslots that apps can override viaHasTemplates- Extended spacing utilities — spacing scale extended with levels 6 (4.5rem), 7 (6rem), and 8 (9rem)
bootstrap.WithCustomCSS()— option to use a custom Sass-compiled CSS file instead of the built-in themesburrow.PageURL()helper — builds pagination URLs that preserve existing query parameters (search, filters) while setting the page number; useful for any paginated view that needs to retain query state across page navigation- ModelAdmin: search input — admin list views with
SearchFieldsnow show a search input with HTMX support; uses FTS5 when available, falls back to LIKE add/subtemplate functions in core — integer arithmetic available in all templates without contrib app registration- Core htmx template stubs —
htmx/jsandhtmx/configdefined as empty stubs in core; htmx app overrides them when registered - Sass build pipeline —
just sasscompiles themes,just sass-setupinstalls Bootstrap Sass source; pre-commit hook auto-compiles on.scsschanges - Alpine.js contrib app — new
alpineapp that embeds Alpine.js 3.15.8 and serves it viastaticfileswith content-hashed URLs; include via{{ template "alpine/js" . }}in layout templates
Changed¶
- Building Releases guide — new documentation page covering local builds, cross-compilation with GoReleaser, automated GitHub releases, version injection, and CI workflows
- Pagination helpers moved to core —
pageURL,pageNumbersare now core template functions (registered inbaseFuncMap);PageResult.PageSize()method replaces the oldpageLimittemplate function;bootstrap/paginationtemplate now usesBasePath/RawQueryinstead ofBaseURLfor query-preserving pagination links themeCSStemplate function removed — the CSS path is now baked into thebootstrap/csstemplate at boot time via an overlay FS; this removes a potential FuncMap collision point for alternative CSS framework apps- Icon template functions moved to
RegisterIconFunc— apps no longer register icon functions viaFuncMap(); instead they callcfg.RegisterIconFunc()inRegister(), which prevents FuncMap collisions when multiple apps use the same icons - Bootstrap layout simplified —
bootstrap/layoutno longer wraps content in<main class="container">; apps control their own container and structure - Auth layout removed —
auth/layouttemplate removed; auth pages now usebootstrap/layoutdirectly - Notes example uses
bootstrap.NavLayout()— app-specific layout replaced by framework-provided nav layout with slot overrides - Notes homepage redesign — full-width hero section with primary color background, shadow cards
Fixed¶
- Auth middleware: HTMX-aware login redirect — when a session expires during HTMX navigation (e.g. in the admin), the auth middleware now uses
HX-Redirectto force a full page navigation to the login page instead of swapping it into<main>
0.5.0 — 2026-03-15¶
Breaking Changes¶
- Forms:
Cleanable.Clean()signature changed —Clean() erroris nowClean(ctx context.Context) error; existing implementations must add thecontext.Contextparameter - Auth:
isAdminEditSelfandisAdminEditLastAdmintemplate functions removed — the custom user admin detail page is replaced by generic ModelAdmin forms;IsAdminEditSelf(),IsAdminEditLastAdmin(), andemailValueare no longer available - Auth:
Usermodel form tags changed —Email,Username,IsActive,Name,Bio, andRolenow have explicitformtags instead ofform:"-"; code that relied on these fields being excluded from forms must useWithExcludeorWithReadOnly - Pagination: cursor-based pagination removed —
ApplyCursor(),TrimCursorResults(),CursorResult(),PageRequest.Cursor,PageResult.NextCursor, andPageResult.PrevCursorhave been removed; useApplyOffset()+OffsetResult()for all pagination Render()renamed —RenderTemplate()is nowRender(); the oldRender(w, r, code, template.HTML)raw-HTML wrapper has been removed (useHTML()directly);RenderTemplate()remains as a deprecated shim with//go:fix inlinefor automatic migration viago fix(Go 1.26+)TemplateExec()renamed —TemplateExecutorFromContext()is nowTemplateExec(); the old name remains as a deprecated shim with//go:fix inline- Minimum Go version raised to 1.26 — required for
reflect.Type.Fieldsiterator,sync.WaitGroup.Go, and future use oftesting.B.Loopandnet/http.CrossOriginProtection RequireAdmin()behavior changed — unauthenticated users are now redirected to/auth/login(likeRequireAuth); previously returned plain-text 403 for all denied users
Added¶
- ModelAdmin:
fmt.Stringersupport in list views — when a list field value implementsfmt.Stringer(e.g. an eager-loaded FK relation), the list view rendersString()instead of the raw struct - ModelAdmin: computed list columns (
ListDisplay) — newListDisplay map[string]func(T) template.HTMLfield onModelAdmin[T]allows custom computed columns in list views that are not direct struct fields auth.Userimplementsfmt.Stringer— returns the user'sNameif set, otherwise falls back toUsername- ModelAdmin: read-only fields (
ReadOnlyFields) — newReadOnlyFields []stringfield onModelAdmin[T]renders specified fields as plain text in create/edit forms; values are preserved from the model instance and cannot be modified by the user - ModelAdmin: CSV/JSON export (
CanExport) — newCanExport boolfield onModelAdmin[T]adds export dropdown to list views; exports respect current filters, search, and sorting; downloads as{slug}-{date}.csvor.json - ModelAdmin: delete confirmation page with cascade impact — when
CanDeleteis true, the delete button now navigates to a dedicated confirmation page instead of using an inlinehx-confirmdialog; ON DELETE CASCADE foreign keys are auto-detected at boot time via SQLite PRAGMAs, and affected row counts are shown on the confirmation page - Forms:
WithReadOnlyoption — newforms.WithReadOnly[T](fields...)option marks fields as read-only; read-only fields skip validation and restore their original value after bind - Forms:
Clean(ctx)andWithCleanFunc—Cleanable.Clean()now receives acontext.Contextfor request-scoped data; newWithCleanFunc[T]option adds closure-based cross-field validation for logic that needs external dependencies (DB, repo); both run after per-field validation and errors are merged - ModelAdmin:
FormOptions— newFormOptions []forms.Option[T]field onModelAdmin[T]allows passing additional form options (e.g.WithCleanFunc) to create/edit forms - Auth: user admin uses generic ModelAdmin — user detail/edit/delete now use ModelAdmin with
WithCleanFuncfor last-admin demotion protection; custom handlers,isAdminEditSelf/isAdminEditLastAdmincontext helpers, and custom template removed - Auth:
authtestpackage — newcontrib/auth/authtestpackage providesNewDB(in-memory DB with auth migrations) andCreateUser(with functional options) for tests that depend on the auth app - ModelAdmin: bulk actions (
BulkActions) — newBulkActions []BulkActionfield onModelAdmin[T]enables multi-select checkbox operations in list views; includesDeleteBulkAction[T]()convenience constructor for bulk delete; toolbar with action dropdown, select-all checkbox, and JS confirm for destructive actions - Error pages with
RenderError()— newRenderError(w, r, code, message)renderserror/{code}templates through the standardRenderpipeline with i18n-translated messages, layout wrapping, and HTMX fragment support; JSON responses forAccept: application/json; default templates for 403, 404, 405, 500 shipped as embedded FS (override by defining{{ define "error/404" }}in any app's templates) - Chi
NotFoundandMethodNotAllowedhandlers — unmatched routes and wrong HTTP methods now render styled error pages instead of plain text - Test helpers:
TestDB,TestErrorExecContext,TestErrorExecMiddleware— shared test helpers in the root package for framework and app tests
Removed¶
- Auth: soft-delete removed from all models —
deleted_atcolumns andbun:",soft_delete"tags removed from User, Credential, RecoveryCode, EmailVerificationToken, and Invite; all deletes are now permanent; includes migration004_drop_soft_delete
Fixed¶
- SQLite PRAGMAs now applied per-connection — per-connection PRAGMAs (foreign_keys, busy_timeout, synchronous, etc.) are now set via
_pragmaDSN parameters instead of one-shotdb.Exec()calls, ensuring they are active on every connection in the pool; this fixesON DELETE CASCADEnot firing when a different pool connection handled the DELETE - ModelAdmin: ambiguous column name with relations —
getItem, list, search (FTS5 and LIKE), and filter queries now qualify column names with?TableAliasto prevent SQLite "ambiguous column name" errors when eager-loading relations that share column names (e.g.id,created_at) - ModelAdmin: pagination preserves query parameters — pagination links now retain search terms, filters, and sort parameters when navigating between pages
- ModelAdmin:
applySortpointer comparison — user-requested column sorting was silently overridden by the defaultOrderBybecauseapplySortreturned the same pointer; now returns(query, bool)for reliable detection - Admin sidebar: active-link prefix matching —
path.indexOf(url) === 0replaced with boundary-awarestartsWith(url + "/")to prevent false matches when model slugs share a prefix (e.g./admin/userand/admin/user-role) - ModelAdmin: delete/bulk-delete preserves current page — after deleting items, the redirect now returns to the current page (clamped to the last available page) instead of always going back to page 1
- Auth: invites FK missing ON DELETE —
used_byandcreated_byin theinvitestable now haveON DELETE SET NULL, preventing user deletion from failing due to FK constraint violations (migration005_invites_fk_set_null) - Auth: swallowed errors in handlers —
SetUserRole(first-user admin promotion),DeleteEmailVerificationToken, andDeleteUserEmailVerificationTokenserrors are now logged viaslog.Errorinstead of silently discarded
0.4.1 — 2026-03-13¶
Added¶
--ratelimit-max-clientsflag — caps the number of tracked client buckets (default 10,000) to prevent memory exhaustion; when the limit is reached, the oldest entry is evicted- Periodic cleanup of expired email verification tokens — the auth background cleanup now also deletes expired tokens, preventing unbounded accumulation
- Ratelimit configuration validation —
Configure()now rejects zero/negative values for rate, burst, and cleanup interval, and negative max-clients
Changed¶
- Add secret key configuration section to deployment guide covering
SESSION_HASH_KEY,SESSION_BLOCK_KEY, andCSRF_KEY
Security¶
- Fix rate limit bypass via X-Forwarded-For spoofing — ratelimit now uses only
X-Real-IPwhen--ratelimit-trust-proxyis enabled;X-Forwarded-Foris no longer used because its multi-value format is trivially spoofed - Fix timing attack on recovery code validation —
ValidateAndUseRecoveryCodenow always iterates all codes to prevent timing side-channel that revealed code position via early return - Fix user enumeration via registration endpoint —
RegisterBeginnow returns HTTP 200 for both new and existing accounts, preventing attackers from probing which usernames or emails are registered - Verify WebAuthn sign count to detect cloned credentials — login now rejects authentication attempts where the sign count does not increase, indicating a potentially cloned authenticator; software authenticators (always 0) are unaffected
- Fix invite token race condition —
MarkInviteUsednow usesWHERE used_at IS NULLto ensure only the first concurrent registration consumes an invite; subsequent attempts fail atomically - Hide CSRF failure reasons from clients — custom error handler returns generic "Forbidden" instead of detailed failure reasons; failure details are logged server-side via slog
0.4.0 — 2026-03-13¶
Breaking Changes¶
/healthzendpoint removed — replaced by/healthz/live(liveness) and/healthz/ready(readiness). Update load balancer and monitoring configurations accordingly.LayoutFuncremoved: TheLayoutFunctype andSetLayout(fn LayoutFunc)are gone. Layouts are now template name strings: callsrv.SetLayout("myapp/layout")with a template name, andRenderTemplatewraps content automatically. Layout templates receive the rendered fragment as.Contentand access dynamic data (navigation, user, etc.) via template functions instead of Go code passing data maps. See the Layouts & Rendering guide.- ModelAdmin migrated to
formspackage:Renderer[T].Form()now takes[]forms.BoundFieldinstead of[]FormFieldand*ValidationError(errors are on eachBoundField).FormField,Choice,AutoFields,PopulateFromFormremoved from modeladmin — useforms.FromModel,forms.BoundField,forms.Choiceinstead.ChoicesFuncandFilterDef.Choicesnow useforms.Choice.
Added¶
ReadinessCheckerinterface — apps can implementReadinessCheck(ctx) errorto contribute to the readiness probe- Healthcheck liveness and readiness endpoints —
/healthz/live(always 200) and/healthz/ready(database + allReadinessCheckerapps, 200/503 with details) NavLinktype andnavLinkstemplate function — core framework now provides filtered, template-ready navigation with automaticAuthOnly/AdminOnlyfiltering and active-state highlighting. Apps only need to implementHasNavItems; no manual filtering orRequestFuncMaprequired for navigation.AuthCheckercontext type — allows core framework to read auth state without importingcontrib/auth. Theauthmiddleware injects it automatically. Custom auth systems can useburrow.WithAuthChecker().formspackage — generic, type-safe form handling withForm[T],BoundField,Choice, struct tag-driven field extraction (form,verbose_name,widget,choices,help_text,validate), request binding viaburrow.Bind, cross-field validation viaCleanableinterface, and dynamic choices viaChoiceProvider/WithChoicesFuncforms.WithExcludeoption — excludes fields by Go struct field name from form renderingcontrib/htmxconfig template —htmx/configtemplate configures htmx to swap422 Unprocessable Entityresponses, enabling consistent status codes for form validation errors across htmx and non-htmx requests- Reusable asset templates —
bootstrap/css,bootstrap/js, andhtmx/jstemplates for including CSS/JS assets in layouts without hardcodingstaticURLcalls
0.3.0 — 2026-03-11¶
Breaking Changes¶
contrib/jobsRepository.Fail()signature changed: AddedbaseDelay time.Durationparameter for configurable exponential backoff. Default backoff changed from2^attemptsseconds (2s, 4s, 8s) tobaseDelay * 2^(attempts-1)with a 30s default (30s, 1m, 2m, 4m).
Added¶
csrfFieldtemplate function — renders a complete<input type="hidden">element with the CSRF token, reducing form boilerplateFieldChoicesonModelAdmin— dynamic<select>dropdowns for foreign key fields, loaded from the database at request time--jobs-retry-base-delayflag — configurable base delay for exponential retry backoff (default: 30s)RenderTemplate()now applies the layout forhx-boostrequests (HX-Boostedheader), fixing navbar disappearing on boosted navigationRequireAuth()middleware usesRefererheader for post-login redirect on non-GET requests, fixing 405 errors on POST-protected routesslog.Infologging for applied database migrations
Changed¶
- Restructure deployment guide: add intro with deployment options table, reorder sections (bare metal → systemd → Docker → graceful restart)
- Add logging guide explaining slog configuration responsibility and handler options
- Add custom LayoutFunc example to layouts guide
- Add template function availability note to i18n guide
- Add working example reference (notes app) to FTS5 guide
- Link to urfave/cli flag documentation in configuration guide
- Add all missing contrib app flags (Jobs, Uploads, Rate Limit, Secure, SMTP Mail) to configuration reference
- Add FieldChoices documentation to admin contrib docs
- Comprehensive tutorial review and fixes (parts 1–7): explicit file paths, missing imports,
go mod tidyin run sections - Add missing setup steps (
go mod init,go get,go mod tidy) to quick start examples in README, index, installation, and quickstart pages - Split notes example app into standard file layout (
models.go,repository.go,handlers.go,app.go) - Add missing
HasJobsinterface to all interface tables in docs - Remove internal "Updating Icons" section from Bootstrap Icons docs
Fixed¶
RenderTemplate()skipped layout forhx-boostrequests, causing navbar to disappear on boosted navigationRequireAuth()redirected to POST URL after login, causing 405 Method Not Allowed- Tutorial Part 5 showed
<nil>in navbar —User.Email(*string) replaced withUser.Username(string) - Tutorial Part 6 used manual route registration with wrong HTTP methods, causing 405 on admin delete
- Replace broken Codeberg URLs with GitHub URLs across documentation
- Convert and resize cover image from PNG (2.9MB) to JPEG (352KB)
0.2.0 — 2026-03-10¶
Breaking Changes¶
- Template engine migration: Replaced Templ with Go's standard
html/template. Alltempl.Componenttypes are replaced bytemplate.HTML. Apps now contribute templates viaHasTemplates, static functions viaHasFuncMap, and request-scoped functions viaHasRequestFuncMap. - LayoutFunc signature changed:
func(title string, content templ.Component) templ.Componentis nowfunc(w http.ResponseWriter, r *http.Request, code int, content template.HTML, data map[string]any) error. - NavItem.Icon type changed:
templ.Componentis nowtemplate.HTML. burrow.Render()replaced byburrow.RenderTemplate(): Handlers now callRenderTemplate(w, r, statusCode, "app/template", data)instead ofRender(w, r, statusCode, component).contrib/jobshandler signature changed:HandlerFuncchanged fromfunc(ctx context.Context, job *Job) errortofunc(ctx context.Context, payload []byte) error. Handlers now receive raw JSON payload bytes instead of the full*Jobstruct.contrib/jobsEnqueue/EnqueueAtreturn type changed: Now returns(string, error)instead of(*Job, error). The string is an opaque job ID.contrib/authemail delivery via jobs: Auth emails are delivered via the job queue when available, with automatic fallback to direct sending. Register aburrow.Queueimplementation (e.g.,jobs.New()) to enable retries and persistence.
Added¶
burrow.Queueinterface — core abstraction for job queues withHandle(),Enqueue(),EnqueueAt(), andDequeue()methods.burrow.HasJobsinterface — apps implementRegisterJobs(q Queue)to declare job handlers, discovered automatically by the queue duringConfigure().burrow.JobHandlerFunc,burrow.JobOption,burrow.JobConfig,burrow.WithMaxRetries()— core job handler types and options.contrib/jobs.Dequeue()— cancel a pending job by its ID.contrib/secure— security response headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, HSTS, CSP, Permissions-Policy, COOP) using unrolled/secure.contrib/htmx— dedicated contrib app with request detection and response helpers, inspired by django-htmx.contrib/jobs— in-process SQLite-backed job queue with worker pool, retry logic, and admin UI via ModelAdmin.contrib/uploads— pluggable file upload storage with local filesystem backend and content-hashed serving.contrib/ratelimit— per-client rate limiting middleware using token bucket algorithm.contrib/authmail— pluggable email renderer interface with SMTP implementation (authmail/smtpmail).contrib/admin/modeladmin— generic Django-style CRUD admin with list fields, filters, search, row actions, and i18n.HasShutdowninterface for graceful app cleanup (called in reverse registration order).HasTemplatesinterface for apps to contribute.htmltemplate files.HasFuncMapinterface for apps to contribute static template functions.HasRequestFuncMapinterface for apps to contribute request-scoped template functions.- Auto-sorting of apps by
HasDependenciesdeclarations inNewServer. - Form binding with validation via
burrow.Bind()andburrow.Validate(). - i18n-aware validation error translation via
ve.Translate(ctx, i18n.TData). - Graceful restart via SIGHUP using tableflip.
- TLS/ACME support for standalone deployment.
- Dark mode toggle with theme persistence in the Bootstrap app.
- Offset and cursor-based pagination helpers.
- Flash messages via
contrib/messageswith Bootstrap alert templates.
Changed¶
- ModelAdmin search auto-detects FTS5 tables at boot time. If a
{tablename}_ftsvirtual table exists, search uses FTS5MATCHinstead ofLIKE, with automatic fallback on syntax errors. - Auth email delivery (verification, invite) now uses the job queue with retries and persistence instead of fire-and-forget goroutines.
contrib/jobsimplementsburrow.Queueinterface; apps register handlers viaHasJobsinstead of manualRegistry.Get()lookups.- Migrated all contrib apps from Templ to
html/template. - Bootstrap Icons are now inline SVG functions returning
template.HTMLinstead oftempl.Component. - Admin panel uses HTMX with explicit
hx-get/hx-targetinstead ofhx-boost. - Replaced
Registry.Bootstrap()withRegistry.RegisterAll(). - Options pattern adopted for
auth.New(),admin.New(),jobs.New(),uploads.New(), andratelimit.New(). - Unified auth context helpers to
context.Contextpattern. - SQLite connection defaults aligned with dj-lite recommendations: added
busy_timeout=5000,temp_store=MEMORY,mmap_size=128MB,journal_size_limit=26MB,cache_size=2000, andIMMEDIATEtransaction mode for better production concurrency. - Rewritten project description (README and docs index) with clear positioning, target audience, and API-only disclaimer.
- Simplified Quick Start to a minimal app without layout, session, or healthcheck.
- New guides: Database, TLS, Routing, Contributing.
- New reference page: Core Functions documenting all exported functions and types.
- Added code examples to every interface in the Core Interfaces reference.
- Added dependency declarations (
Depends on:) to all contrib app docs. - Reorganized guide sidebar into Core, Templates & UI, Advanced, and Deployment groups.
- New guide: Full-Text Search covering FTS5 virtual tables, triggers, sanitization, highlighting, and performance.
- Added copyright footer to documentation site.
- New guide: Coming from Django, mapping Django concepts to Burrow equivalents with side-by-side code examples.
- Added request lifecycle diagram to the Routing guide.
- Added "Why urfave/cli?" section to Server & Registry reference.
- New guide: Testing covering test helpers and patterns.
- New pages: Examples & Tutorial overview, seven-part Tutorial.
- Expanded auth page rendering and Auth Layout documentation with usage examples.
- Default email renderer moved from
authmail/smtpmail/templatestoauth.DefaultEmailRenderer(). Theauthmailpackage keeps theRendererinterface only. - Auth app now declares
i18nas a dependency alongsidesession. - Added
i18n.NewTestApp()helper for creating a minimal i18n setup in tests.
Fixed¶
- Auth emails (verification, invite) are now rendered in the user's locale. Previously, emails were always in English because goroutines used
context.Background(), losing the request locale. - Auth pages now render with a minimal layout instead of full app chrome.
- WebAuthn cleanup goroutine uses context-based cancellation.
buildManifesterrors are propagated instead of silently discarded.Seedis called onSeedableapps during server bootstrap.- Fixed broken cross-links and removed redundant content across docs.
0.1.0 — 2026-02-19¶
- App-based architecture with
burrow.Appinterface and optional interfaces. - Pure Go SQLite via
modernc.org/sqlite(no CGO required). - Per-app SQL migration runner with
_migrationstracking table. - Chi v5 router integration with
burrow.HandlerFuncerror-returning handlers. - Cookie-based sessions via
gorilla/sessions. - WebAuthn/passkey authentication with recovery codes.
- CSRF protection via
gorilla/csrf. - i18n with Accept-Language detection and
go-i18ntranslations. - Content-hashed static file serving.
- Admin panel coordinator.
- CLI configuration via
urfave/cliwith flag, env var, and TOML support. - CSS-agnostic layout system.