Introducing Burrow - a Web Framework for the Small Web
I've been writing about digital independence on this blog for a while now. About moving away from Big Tech, about self-hosting, about the idea that downloading and running software shouldn't require a degree in container orchestration. But at some point, writing about it isn't enough. You have to build something.
Burrow, is that something.
The itch
I wanted to build a few web applications for myself. A read-it-later service. A feed reader. A web-based pod catcher. Nothing exotic - these are solved problems, and there are dozens of SaaS offerings for each. But that's precisely the issue: I don't want another subscription to a service that almost does what I need. I want software that does exactly what I need, running on my own hardware, under my control.
So the question became, what do I build these with?
Wait - aren't you the Python guy? Why not Django?
Fair question. I've built plenty of Django applications over the years, and it remains an excellent framework. But Django has a deployment problem. You need a Python installation, a virtual environment, a process manager, probably a reverse proxy in front of it. Or you reach for Docker, and suddenly, your simple little app requires an entire orchestration layer just to run.
Go solves this. You compile your application, copy the binary to your server, and run it. That's it. No runtime, no dependencies, no virtualenv. For the kind of small, personal applications I want to build, that matters enormously.
What Burrow is
Burrow is a web framework for Go that takes cues from Django, Rails, and Flask - but targets single-binary deployment. Server-rendered HTML with templates, modular apps with their own routes, migrations, and middleware, and an embedded SQLite database. The result is an application you deploy by copying one file.
The name isn't accidental, by the way. A burrow is a network of interconnected chambers, each with its own purpose, yet part of a larger whole. That's how the framework works: pluggable apps are the rooms, and your gophers live in them.
It's built on Chi for routing, Bun with modernc.org/sqlite for the database (pure Go, no CGO), and Go's standard html/template for rendering. It comes with contrib apps for things you'd otherwise have to wire up yourself: sessions, authentication with passkeys, CSRF protection, internationalization, an admin panel, background jobs, and more.
To give you an idea of what working with Burrow looks like, here's a simple note app. First, the model - if you've used Django or any ORM, this will feel familiar:
type Note struct {
bun.BaseModel `bun:"table:notes"`
ID int64 `bun:",pk,autoincrement"`
Title string `bun:",notnull" form:"title" validate:"required"`
Content string `form:"content"`
CreatedAt time.Time `bun:",nullzero,default:current_timestamp"`
}Each Burrow app is a self-contained module with its own routes, templates, and migrations. The app struct registers itself with the framework and declares what it needs:
//go:embed migrations
var migrationFS embed.FS
//go:embed templates
var templateFS embed.FS
type App struct {
db *bun.DB
}
func New() *App { return &App{} }
func (a *App) Name() string { return "notes" }
func (a *App) MigrationFS() fs.FS { sub, _ := fs.Sub(migrationFS, "migrations"); return sub }
func (a *App) TemplateFS() fs.FS { sub, _ := fs.Sub(templateFS, "templates"); return sub }
func (a *App) Register(cfg *burrow.AppConfig) error {
a.db = cfg.DB
return nil
}
func (a *App) Routes(r chi.Router) {
r.Route("/notes", func(r chi.Router) {
r.Method("GET", "/", burrow.Handle(a.list))
r.Method("GET", "/new", burrow.Handle(a.form))
r.Method("POST", "/", burrow.Handle(a.create))
})
}The handlers are plain HTTP handlers that return errors instead of swallowing them. Burrow takes care of template rendering, form binding, and validation:
func (a *App) list(w http.ResponseWriter, r *http.Request) error {
var notes []Note
err := a.db.NewSelect().Model(¬es).
OrderExpr("created_at DESC").Scan(r.Context())
if err != nil {
return err
}
return burrow.RenderTemplate(w, r, http.StatusOK, "notes/list", map[string]any{
"Notes": notes,
})
}
func (a *App) create(w http.ResponseWriter, r *http.Request) error {
var note Note
if err := burrow.Bind(r, ¬e); err != nil {
return burrow.RenderTemplate(w, r, http.StatusUnprocessableEntity, "notes/form", map[string]any{
"Error": err,
})
}
_, err := a.db.NewInsert().Model(¬e).Exec(r.Context())
if err != nil {
return err
}
http.Redirect(w, r, "/notes", http.StatusSeeOther)
return nil
}And in main.go, you plug the notes app into the server - alongside any contrib apps you need:
func main() {
srv := burrow.NewServer(
notes.New(),
)
cmd := &cli.Command{
Name: "notes",
Flags: srv.Flags(nil),
Action: srv.Run,
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}Run go build, copy the binary to your server, and start it. Templates, migrations, static files - everything is embedded in that one binary. A more complete example can be found in the repository on GitHub.
Why this matters
In my Going Sovereign post, I argued that we as developers need to build software that ordinary people can actually install and use - the way shareware used to work. Download, run, follow a short setup wizard, done. Burrow is my attempt to make that kind of software easier to build.
Most Go web development today follows the API-backend-plus-SPA-frontend pattern. I find that unnecessarily complex for many use cases. Do you really need a React app with a build pipeline, a separate API server, and CORS configuration just to display a list of bookmarks? Burrow takes the other path: render HTML on the server, send it to the browser, done.
Michael Kennedy recently wrote about hyper-personal software - small applications built by individuals to solve their own specific problems. He calls the results "dark matter software" because most of it is invisible, built for an audience of one. With agentic coding tools and a framework like Burrow, building these hyper-personal apps becomes straightforward. You describe what you want, the agent scaffolds a Burrow app, you compile it, and you've got a self-contained binary that does exactly what you need.
That's the world I want to live in: software you own, running on hardware you control, doing precisely what you asked it to.
Where things stand
Burrow is somewhere between alpha and beta. I'm using it myself to build the applications I described earlier, and the first of those should be available this week. I'm tagging version 0.3.0 tomorrow, which feels like a reasonable moment to start talking about it publicly.
The documentation is at burrow.someonewho.codes, including a tutorial that walks you through building a complete application from scratch. The source code lives on GitHub.
If you're a Go developer who's tired of wiring up the same boilerplate for every project, or if you're someone who wants to build small, self-hosted applications without the overhead of a typical web stack, give Burrow a look. And if you end up building something for the Small Web with it, I'd love to hear about it.