Built for raw speed. Shipped clean.
Breeze is an event-driven HTTP framework for Go built on gnet. Zero-allocation hot paths, a composable middleware chain, native WebSocket support with a built-in hub, and Scalar docs — out of the box.
Every decision made for performance.
Not another wrapper around net/http. Breeze is built from the ground up on a non-blocking event loop with an obsession over allocations.
gnet Event Loop
Non-blocking I/O with one event-loop goroutine per CPU core. RoundRobin load balancing across reactors.
Zero-Alloc Router
Path segment matching uses a stack-allocated [16]string. Param indexes are pre-computed at registration.
Zero-Copy Parsing
unsafe.String converts raw bytes to strings without copying. Header keys are lowercased in-place.
Worker Pool
Fixed goroutine pool with 16× buffered channel absorbs bursts. Queue-full fallback spawns a goroutine rather than blocking the event loop.
Native WebSocket
RFC 6455 WebSocket built directly into the gnet event loop. Zero goroutine-per-connection.
OpenAPI 3.1 Built-In
Annotate routes with Go structs. Breeze reflects types at startup to generate a live Scalar spec.
Template Engine
Server-rendered views and components with a built-in SPA runtime, partial rendering, and a client-side reactive store.
9 Middleware Suite
JWT auth, rate limiting, Brotli/Gzip/Deflate compression, ETag cache, CORS, security headers, logger, panic recovery.
3× faster. Not a typo.
Measured on a simple JSON endpoint, same machine, same payload.
- Breeze~630k req/s
- Fiber~210k req/s
- Echo~140k req/s
- Gin~120k req/s
- net/http~85k req/s
* Approximate. Run benchmarks in your environment for authoritative numbers.
Installation
Requires Go 1.24.3 or later.
go get github.com/nelthaarion/breeze
The module pulls in gnet v2 for the event loop, go-json for fast JSON marshaling, brotli for compression, and golang-jwt/jwt for authentication.
Quick Start
A complete working server in under 20 lines.
package main
import (
"runtime"
"github.com/nelthaarion/breeze"
middleware "github.com/nelthaarion/breeze/middlewares"
)
func main() {
router := breeze.NewRouter()
router.Use(middleware.RecoveryMiddleware())
router.Use(middleware.LoggingMiddleware())
router.Handle(breeze.GET, "/", func(ctx *breeze.Context) {
ctx.JSON(map[string]string{"status": "ok"})
})
router.Handle(breeze.GET, "/users/:id", func(ctx *breeze.Context) {
ctx.JSON(map[string]string{"id": ctx.Param("id")})
})
pool := breeze.NewWorkerPool(runtime.NumCPU())
app := breeze.New(router, pool)
app.Run(3000, true) // port, multiCore
}
Server Config
app.Run(port, multiCore) starts the gnet event loop with sensible defaults baked in.
| Option | Value | Effect |
|---|---|---|
| TCPNoDelay | enabled | Disables Nagle's algorithm — lower latency for small messages |
| Multicore | configurable | Spawns one event-loop per CPU core when true |
| LoadBalancing | RoundRobin | Distributes connections evenly across event loops |
app.Run(8080, true) // multicore — one loop per CPU, recommended app.Run(8080, false) // single-core — useful for dev / debugging
Router
Create a router, register global middleware, then define routes. Routes support static segments, named parameters, and wildcards.
Methods
router := breeze.NewRouter()
// Global middleware (runs on every route)
router.Use(middleware.LoggingMiddleware())
// Static segment
router.Handle(breeze.GET, "/health", healthHandler)
// Named parameter — :id available via ctx.Param("id")
router.Handle(breeze.GET, "/users/:id", getUser)
router.Handle(breeze.POST, "/users", createUser)
router.Handle(breeze.PUT, "/users/:id", updateUser)
router.Handle(breeze.DELETE, "/users/:id", deleteUser)
// Wildcard — *filepath captures everything after /files/
router.Handle(breeze.GET, "/files/*filepath", fileHandler)
// Per-route middleware (runs only on this route)
router.Handle(breeze.POST, "/admin/action", adminHandler,
authMiddleware, auditMiddleware,
)
Route Matching Priority
Routes are matched in registration order. A static segment always beats a named parameter when both could match — register more specific routes first.
| Pattern | Matches | Params |
|---|---|---|
/users | /users | — |
/users/:id | /users/abc | id=abc |
/users/:id/posts | /users/abc/posts | id=abc |
/files/*path | /files/a/b/c.txt | path=a/b/c.txt |
Context
Every handler receives a *breeze.Context. It carries the connection, parsed request, response, route params, and controls the middleware chain.
Response Helpers
// JSON — sets Content-Type: application/json, status 200
ctx.JSON(map[string]any{"id": 1, "name": "Alice"})
// Plain text
ctx.WriteString("Hello, World!")
// HTML
ctx.HTML([]byte("<h1>Hello</h1>"))
// Override status code (call after JSON/WriteString/HTML)
ctx.Status(201)
// Add/override a header
ctx.SetHeader("X-Request-Id", "uuid-123")
Middleware Chain
// Advance to the next handler in the chain
ctx.Next()
// Short-circuit — skip all remaining handlers
ctx.Abort()
// Typical middleware pattern
func AuthMiddleware(ctx *breeze.Context) {
token := ctx.Req.Header["authorization"]
if token == "" {
ctx.Status(401)
ctx.WriteString("Unauthorized")
ctx.Abort() // ← stops the chain here
return
}
ctx.Next() // ← continue to next handler
}
Request
ctx.Req is a *breeze.HTTPRequest parsed from raw bytes with zero unnecessary allocations.
method := ctx.Req.Method // breeze.Method ("GET", "POST", …)
path := ctx.Req.Path // "/users/123"
ct := ctx.Req.Header["content-type"] // headers are lowercased
var payload CreateUserRequest
json.Unmarshal(ctx.Req.Body, &payload)
page := ctx.Query("page") // "2"
limit := ctx.Query("limit") // "20"
Response
ctx.Res is a *breeze.HTTPResponse. Breeze serializes it to raw HTTP/1.1 bytes using strconv.AppendInt — no fmt.Sprintf, pre-sized buffer, array-indexed status text.
| Field | Type | Notes |
|---|---|---|
| Status | int | HTTP status code |
| Headers | map[string]string | Copy-on-write — safe to mutate via SetHeader |
| Body | []byte | Raw response body |
You rarely set ctx.Res directly — use the helpers (JSON, WriteString, HTML, Status, SetHeader) instead.
Params & Query
// Route: /users/:id/posts/:postId
userID := ctx.Param("id") // "abc"
postID := ctx.Param("postId") // "42"
// Query: /search?q=go&page=2
q := ctx.Query("q") // "go"
page := ctx.Query("page") // "2"
// Middleware can pass data downstream
ctx.SetParam("userID", "from-auth")
uid := ctx.GetParam("userID") // "from-auth"
all := ctx.GetParams() // map[string]string (copy)
Template Engine new
Breeze ships a full server-side HTML template engine powered by Go's html/template package. It supports layouts, reusable components, automatic SPA partial rendering, a reactive client-side data store, and hot-reload dev mode — with zero external dependencies.
Parse
Views, components, and layout are parsed once at startup (or per-request in dev mode).
Render
A handler calls ctx.Render or router.View. Data is passed as any Go value.
Layout wrap
The view is embedded inside the layout via {{template "content" .}}. Components are injected inline.
SPA inject
The runtime script, page data JSON, and template sources are injected just before </body>.
Navigate
Link clicks fetch a partial (content block only). The client swaps #breeze-app without a full reload.
Setup & Config
Create a TemplateEngine with NewTemplateEngine, then wire it into the router. All fields have sensible defaults.
engine := breeze.NewTemplateEngine(breeze.TemplateConfig{
ViewsDir: "views", // default: "views"
ComponentsDir: "components", // default: "components"
LayoutFile: "views/layout.html", // default: ViewsDir/layout.html
// set "" to disable layout wrapping
DevMode: false, // true: re-parse on every render (hot reload)
FuncMap: template.FuncMap{ // optional: add custom template funcs
"upper": strings.ToUpper,
},
})
| Field | Type | Default | Description |
|---|---|---|---|
ViewsDir | string | "views" | Directory containing view .html files |
ComponentsDir | string | "components" | Directory containing component .html files |
LayoutFile | string | "views/layout.html" | Shared layout wrapper. Empty string disables it. |
DevMode | bool | false | Re-parses templates on every request — enables hot reload |
FuncMap | template.FuncMap | nil | Additional custom template functions |
Directory Structure
Breeze expects two separate directories — one for full-page views and one for reusable components. The layout file lives inside the views directory.
layout.html ← defines "layout" block (wraps all views)
home.html ← defines "content" block
about.html
users.html
components/
nav.html ← defines "nav" block
card.html ← defines "card" block
stats.html ← defines "stats" block
users-table.html ← defines "users-table" block
public/ ← static assets (CSS, images, JS)
Every component file uses {{define "name"}}…{{end}}. View files use {{define "content"}}…{{end}}. The layout file uses {{define "layout"}}…{{end}}.
Views & Layout
A view is a .html file inside ViewsDir that defines a "content" block. The layout wraps every view by embedding that block via {{template "content" .}}.
Layout file — views/layout.html
{{define "layout"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
<link rel="stylesheet" href="/public/app.css">
</head>
<body>
{{component "nav" .}}
<div id="breeze-app">
{{template "content" .}} <!-- view content goes here -->
</div>
</body>
</html>
{{end}}
The #breeze-app element is the SPA swap target. On every SPA navigation the client replaces its innerHTML with the new view's content block. You can name it anything — Breeze also falls back to <main> and then <body> if #breeze-app is absent.
View file — views/home.html
{{define "content"}}
<div class="page">
<h1>Welcome, {{ .Data.Name }}!</h1>
{{component "card" (map "title" "Hello" "body" "World")}}<!-- component with inline data -->
<ul>
{{range .Data.Items}}
<li>{{ .Name }} — ${{ .Price }}</li>
{{end}}
</ul>
</div>
{{end}}
Inside templates, .Data holds the value returned by your handler's data function. The outer . is a TemplateData struct which also exposes .IsPartial.
TemplateData wrapper
Every render wraps your data in a TemplateData struct before passing it to the template. You access your data through .Data, not . directly:
| Field / Method | Type | Description |
|---|---|---|
.Data | any | The value returned by your handler's data function |
.IsPartial | bool | true when the request carries X-Breeze-Partial: true (SPA navigation) |
Disabling the layout
Set LayoutFile: "" in TemplateConfig to render views without a shared layout. The view template itself is executed directly.
Components
Components are reusable HTML fragments that live in ComponentsDir. Each file defines a named template block using {{define "name"}}…{{end}}.
Component file — components/card.html
{{define "card"}}
<div class="card">
<h3>{{ .title }}</h3>
<p>{{ .body }}</p>
</div>
{{end}}
Using a component inside a view
Use the built-in component function (or its alias partial) to embed a component inline:
<!-- Pass the parent data straight through -->
{{component "nav" .}}
<!-- Build a map inline with the map helper -->
{{component "card" (map "title" "Hello" "body" "World")}}
<!-- Pass a field from your page data -->
{{component "stats" .Data.StatsPayload}}
<!-- partial is an alias for component -->
{{partial "card" (map "title" "Alt syntax" "body" "Same thing")}}
Rendering a component from a handler
To serve a component as a bare HTML fragment (no layout, no SPA script) — useful for polling endpoints:
engine.RenderComponent(ctx, "stats", StatsData{Count: 42, Time: "12:00:00"})
Built-in Template Functions
Breeze registers these functions automatically in addition to Go's standard template functions:
| Function | Signature | Description |
|---|---|---|
component |
(name string, data any) HTML |
Renders the named component with the given data. Returns safe HTML. |
partial |
(name string, data any) HTML |
Alias for component. Both names work identically. |
map |
(...any) map[string]any |
Constructs a map[string]any from alternating key-value pairs. Keys must be strings. Pairs must be even in number. |
Custom functions
Add your own functions via TemplateConfig.FuncMap:
engine := breeze.NewTemplateEngine(breeze.TemplateConfig{
FuncMap: template.FuncMap{
"upper": strings.ToUpper,
"format": func(t time.Time) string { return t.Format("Jan 2, 2006") },
"dollars": func(cents int) string { return fmt.Sprintf("$%.2f", float64(cents)/100) },
},
})
// Use in a template:
// {{upper .Data.Name}}
// {{format .Data.CreatedAt}}
Route Helpers
Three methods on *Router integrate the template engine with routing:
router.View — full-page view route
Registers a GET route that renders a named view, wrapped in the layout. The optional data function receives the request context and returns data for the template.
// Static view — no data
router.View("/", engine, "home", nil)
// Dynamic view — data function called on each request
router.View("/about", engine, "about", func(ctx *breeze.Context) any {
return map[string]any{"title": "About Us", "Year": 2026}
})
// View with database fetch
router.View("/users", engine, "users", func(ctx *breeze.Context) any {
return db.GetAllUsers()
})
router.Fragment — bare HTML fragment route
Registers a GET route that renders a single component as a bare HTML fragment — no layout, no SPA script injection. Designed to be consumed by breeze.fetch() or breeze.poll() on the client.
// Serve the "stats" component at /fragments/stats
router.Fragment("/fragments/stats", engine, "stats", func(ctx *breeze.Context) any {
return StatsData{Count: getActiveUsers(), Time: time.Now().Format("15:04:05")}
})
// Then poll it from any template:
// <div id="stats-box"></div>
// <script>breeze.poll('/fragments/stats', '#stats-box', 2000)</script>
ctx.Render — render from a handler
Renders a view directly inside any handler, with full control over what data is passed.
router.Handle(breeze.GET, "/search", func(ctx *breeze.Context) {
q := ctx.Query("q")
ctx.Render(engine, "home", map[string]any{
"SearchQuery": q,
"Results": db.Search(q),
})
})
Page Data
When a full page is rendered, Breeze serializes your data as JSON and embeds it in the page inside a non-executing script tag. This makes the data available to client-side JavaScript without a separate API call.
<script id="__breeze_data__" type="application/json">{"name":"Alice","count":42}</script>
Read it client-side with breeze.data():
const all = breeze.data(); // { name: "Alice", count: 42 }
const name = breeze.data("name"); // "Alice"
The data object is also available from any breeze.watch() subscriber and can be replaced with breeze.setData(newData).
SPA Runtime
On every full-page render, Breeze injects a self-contained JavaScript runtime just before </body>. It turns your multi-page app into a single-page app automatically — no configuration required.
How SPA navigation works
- Every
<a href="…">click is intercepted by a document-level event listener. - The runtime sends a
fetch()to the same URL with the headerX-Breeze-Partial: true. - The server detects the header and returns only the
{{define "content"}}…{{end}}block — no layout, no runtime script. - The client replaces
#breeze-app'sinnerHTMLwith the fragment and callshistory.pushState. - The browser URL updates; back/forward navigation is handled via
popstate.
Opt out of SPA navigation
Add data-no-spa to any link to trigger a full browser navigation instead:
<a href="/download/report.pdf" data-no-spa>Download PDF</a>
Links that are already excluded automatically: external URLs, target="_blank", anchors (href="#..."), mailto:, tel:.
Lifecycle hooks
Register callbacks on the global Breeze object (note the capital B) before navigation fires:
// Return false from any before-hook to cancel the navigation / submit
Breeze.onBeforeNavigate(function(e) {
console.log('navigating to', e.url);
// return false; // ← uncomment to cancel
});
Breeze.onAfterNavigate(function(e) {
console.log('navigated to', e.url);
analytics.track('pageview', e.url);
});
Breeze.onBeforeSubmit(function(e) {
console.log('submitting', e.form, 'to', e.url);
});
Breeze.onAfterSubmit(function(e) {
console.log('submitted to', e.url, 'html length:', e.html.length);
});
Loading state
During any navigation or form fetch, Breeze adds breeze-loading to document.body. Use it in CSS to show a visual indicator:
/* Dim the content while loading */
body.breeze-loading #breeze-app {
opacity: 0.55;
transition: opacity .15s;
}
/* Or show a top progress bar */
body.breeze-loading::before {
content: '';
position: fixed; top: 0; left: 0; right: 0; height: 3px;
background: var(--accent);
animation: loading-bar .8s ease infinite;
}
DOM events
| Event | Detail | Fired when |
|---|---|---|
breeze:navigate | { url } | After every SPA page or form navigation |
breeze:update | { url, target, html } | After breeze.fetch() swaps a fragment |
breeze:render | { name, target, html, local } | After breeze.render() swaps a view or component |
breeze:ws:open | { path } | WebSocket connection opened |
breeze:ws:message | { data, path } | WebSocket message received |
breeze:ws:close | { path, code } | WebSocket connection closed |
Script Lifecycle
When the SPA runtime swaps content into #breeze-app, it re-runs scripts found in the new fragment according to these rules:
| Script type | Attribute | Behaviour on swap |
|---|---|---|
External (src="…") | — | Loaded once. Subsequent swaps remove the duplicate node; the script is not re-executed. |
| Inline | data-spa-run="always" | Re-executed on every swap. Use for polling setup, re-initialisation. |
| Inline | data-spa-run="once" | Executed once per page lifecycle. Tracked by content hash or data-spa-id. |
| Inline | none | Never re-executed after initial page load. Safe default for WebSocket setup, analytics init. |
Module (type="module") | — | Deduplicated like external scripts. |
<!-- Runs on every SPA navigation — good for starting polls -->
<script data-spa-run="always">
breeze.poll('/fragments/stats', '#stats-box', 2000);
</script>
<!-- Runs once per page lifecycle — good for analytics, one-time init -->
<script data-spa-run="once">
analytics.init({ page: window.location.pathname });
</script>
<!-- No attribute — only runs on hard page load (default) -->
<script>
const ws = breeze.ws('/ws', { onMessage: e => console.log(e.data) });
</script>
Tip: Use data-spa-id="my-init" on a data-spa-run="once" script to give it a stable identity. Without it, Breeze uses a content hash — which means any whitespace change causes it to re-run.
SPA Forms
The runtime automatically intercepts <form> submits and handles them without a full page reload — progressive enhancement means forms still work without JavaScript.
GET forms
Form fields are serialized to a query string and the result is navigated to via the SPA router:
<form action="/search" method="GET"> <input name="q" placeholder="Search…"> <button type="submit">Search</button> </form> <!-- Navigates to /search?q=value without a reload -->
POST forms
Submitted via fetch(). The response HTML is swapped into #breeze-app. Control the request body encoding with data-content-type:
<!-- Default: application/x-www-form-urlencoded --> <form action="/contact" method="POST"> <input name="email" type="email"> <button type="submit">Subscribe</button> </form> <!-- JSON body: form fields serialised as a JSON object --> <form action="/api/users" method="POST" data-content-type="application/json"> <input name="name" type="text"> <input name="email" type="email"> <button type="submit">Create user</button> </form>
Opting out
The runtime skips interception automatically for:
enctype="multipart/form-data"— file uploads are always sent as a real browser submit.target="_blank"— opens in a new tab, not swapped in.- External
actionURLs (different origin). data-spa="false"— explicit opt-out for any form.
<form action="/download" method="POST" data-spa="false"> <button type="submit">Download (full reload)</button> </form>
Client API — breeze.*
The runtime injects a window.breeze object with utilities for fetching fragments, polling, navigation, reactive data, re-rendering, and WebSocket connections.
Fragment fetching
// Fetch a fragment and swap it into a target element
await breeze.fetch('/fragments/stats', '#stats-box');
// With options
await breeze.fetch('/api/partial', '#target', {
method: 'POST',
body: JSON.stringify({ id: 42 }),
headers: { 'Content-Type': 'application/json' },
onSuccess: (html, el) => console.log('updated', el),
onError: (err, el) => console.error(err),
});
Polling
// Auto-refresh every 2 seconds
breeze.poll('/fragments/stats', '#stats-box', 2000);
// Stop polling on a target element
breeze.stop('#stats-box');
Navigation & swap
// Programmatic SPA navigation (pushes history)
breeze.navigate('/about');
// Directly swap HTML into an element (no fetch)
breeze.swap('#target', '<p>Hello</p>');
Reactive data store
// Read page data (embedded as JSON at render time)
const data = breeze.data(); // entire object
const name = breeze.data("name"); // single key
// Replace store and optionally re-render a component
await breeze.setData({ count: 99 }, '#stats-box', 'stats');
// Watch for data changes
const unsubscribe = breeze.watch(data => {
document.title = `Count: ${data.count}`;
});
// Later: unsubscribe();
Client-side re-render
Renders a view or component on the client using embedded template sources — no server round-trip. If the template source is not embedded, falls back to the POST /breeze/render endpoint automatically.
// Re-render the "card" component with new data into #my-card
await breeze.render('card', { title: 'Updated', body: 'New content' }, '#my-card');
// Re-render using the current store data
await breeze.render('stats', undefined, '#stats-box');
WebSocket
const conn = breeze.ws('/ws', {
onOpen: (e) => console.log('connected'),
onMessage: (e) => console.log('msg:', e.data),
onClose: (e) => console.log('closed'),
onError: (e) => console.error(e),
});
conn.send('hello server'); // send a text message
conn.close(); // gracefully close
// conn.socket — access the raw WebSocket object
The WebSocket connection auto-reconnects with exponential backoff (1 s → 30 s max) if the connection drops.
Full client API reference
| Function | Signature | Description |
|---|---|---|
breeze.fetch | (url, target?, options?) → Promise<string> | Fetch a fragment and swap it into target |
breeze.poll | (url, target, intervalMs, options?) | Auto-refresh a fragment on an interval |
breeze.stop | (target) | Stop polling on the given element |
breeze.swap | (target, html) | Directly replace innerHTML (no fetch) |
breeze.navigate | (url) | Programmatic SPA navigation with pushState |
breeze.data | (key?) → any | Read page data embedded at render time |
breeze.setData | (data, target?, name?) → Promise | Replace store; optionally trigger re-render |
breeze.render | (name, data?, target?) → Promise<string> | Client-side or server-side re-render |
breeze.watch | (fn) → unsubscribe | Subscribe to data store changes |
breeze.ws | (path, handlers?) → { send, close, socket } | Open a WebSocket with auto-reconnect |
Re-render Endpoint
Call router.EnableReRender(engine) once at startup to register POST /breeze/render. The client-side breeze.render() uses this endpoint when the template source is not embedded locally.
engine := breeze.NewTemplateEngine(breeze.TemplateConfig{...})
router.EnableReRender(engine) // registers POST /breeze/render
The endpoint accepts a JSON body with either "component" or "view" (component takes precedence):
// Render a component
{ "component": "card", "data": { "title": "Hello", "body": "World" } }
// Render a view's content block
{ "view": "home", "data": { "name": "Alice" } }
You can also call it directly from JavaScript without using breeze.render():
const res = await fetch('/breeze/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ component: 'stats', data: { Count: 99, Time: '12:00' } }),
});
document.querySelector('#stats-box').innerHTML = await res.text();
Client-side template evaluation: Breeze embeds the raw source of every view and component in the page as a JSON blob (<script id="__breeze_tmpl__" type="application/json">). breeze.render() checks this first and evaluates the template locally using a built-in Go-template subset interpreter — supporting {{ .Field }}, {{range …}}…{{end}}, {{if …}}…{{end}}, and nested component / partial calls. Only if the template is not embedded does it fall back to the server.
Dev Mode
Set DevMode: true in TemplateConfig to disable template caching. Every request re-parses the template files from disk, so changes to .html files are reflected immediately without restarting the server.
engine := breeze.NewTemplateEngine(breeze.TemplateConfig{
DevMode: true, // disable in production — parsing on every request is expensive
})
Always set DevMode: false (the default) in production. In production, templates are parsed once on first use and cached in memory, making rendering near-zero cost.
Preload
Call engine.Preload() at startup (after all routes are registered) to parse all views and components eagerly. This surfaces template errors on startup rather than on the first request, and warms the cache so the first users don't pay the parse cost.
engine := breeze.NewTemplateEngine(breeze.TemplateConfig{...})
if err := engine.Preload(); err != nil {
log.Fatalf("template parse error: %v", err)
}
// Register routes after Preload — Preload doesn't need to know about routes
router.View("/", engine, "home", nil)
Preload is a no-op in DevMode — templates are re-parsed on every request anyway.
Middleware
Every middleware is a HandlerFunc — composable, testable, and zero-dependency on each other.
import (
"github.com/golang-jwt/jwt/v5"
middleware "github.com/nelthaarion/breeze/middlewares"
)
accessToken, _ := middleware.GenerateJWT(
"secret", jwt.MapClaims{"user_id": "abc", "role": "admin"},
15*time.Minute, nil,
)
refreshToken, _ := middleware.GenerateRefreshToken("refresh-secret",
jwt.MapClaims{"user_id": "abc"}, 7*24*time.Hour, nil)
router.Use(middleware.JWTAuthMiddleware(middleware.JWTOptions{
AccessSecret: "secret",
RefreshSecret: "refresh-secret",
SigningMethod: jwt.SigningMethodHS256,
EnableRefreshToken: true,
RequiredRoles: []string{"admin"},
UserContextKey: "user",
ClaimsValidator: func(claims jwt.MapClaims) bool {
return claims["active"] == true
},
OnUnauthorized: func(ctx *breeze.Context, err error) {
ctx.Status(401)
ctx.JSON(map[string]string{"error": err.Error()})
},
}))router.Use(middleware.CORSMiddleware(middleware.CORSOptions{
AllowOrigins: "https://myapp.com",
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Content-Type,Authorization",
AllowCredentials: "true",
MaxAge: "86400",
}))
// OPTIONS preflight is handled automatically → 204 No Contentrouter.Use(middleware.NewRateLimiter(middleware.RateLimiterOptions{
Requests: 100,
Per: time.Minute,
Message: "Slow down — rate limit exceeded",
}))
// Different limits per route
router.Handle(breeze.POST, "/login", loginHandler,
middleware.NewRateLimiter(middleware.RateLimiterOptions{
Requests: 5,
Per: time.Minute,
}),
)
// Returns 429 Too Many Requests when exceeded// Priority: br (Brotli) → gzip → deflate → none router.Use(middleware.CompressionMiddleware()) // Sets Content-Encoding response header automatically
router.Use(middleware.DefaultSecurityMiddleware())
// Or fully custom
router.Use(middleware.SecurityMiddleware(middleware.SecurityOptions{
ContentSecurityPolicy: "default-src 'self'",
XFrameOptions: "DENY",
XContentTypeOptions: "nosniff",
StrictTransportSecurity: "max-age=63072000; includeSubDomains; preload",
ReferrerPolicy: "no-referrer",
XXSSProtection: "1; mode=block",
}))router.Use(middleware.ScalarMiddleware(router, middleware.ScalarOptions{
Title: "My API",
Version: "1.0.0",
JSONPath: "/Scalar.json",
UIPath: "/Scalar",
}))
router.Handle(breeze.POST, "/users", createUser,
middleware.DocPOST("/users", Scalar.RouteDoc{
Title: "Create user",
Tags: []string{"Users"},
Input: []Scalar.InputGroup{
{Type: Scalar.InputBody, Fields: CreateUserRequest{}, Required: true},
},
Output: UserResponse{},
OutputStatus: 201,
}),
)
// Visit /Scalar → live Scalar UIapp := breeze.New(router, pool)
hub := app.WebSocket("/ws", &breeze.WSHandlerFunc{
Connect: func(conn *breeze.WSConn) {
conn.SendText("welcome")
},
Message: func(conn *breeze.WSConn, opcode byte, payload []byte) {
hub.BroadcastText(string(payload))
},
Close: func(conn *breeze.WSConn, code uint16, reason string) {
hub.BroadcastText("a user left")
},
})Logger middleware
Logs method, path, status, and elapsed time for every request. Output goes to stdout in RFC3339 format.
router.Use(middleware.LoggingMiddleware()) // Output: [Breeze][2026-06-23T12:00:00Z] GET /users -> 200 (1.2ms)
Panic Recovery middleware
Wraps the entire handler chain in a defer/recover. On panic: logs the value + full stack trace, sets 500, calls ctx.Abort(). Your server never crashes from a handler bug.
router.Use(middleware.RecoveryMiddleware()) // Register first so it wraps everything else
CORS middleware
Sets Access-Control-* headers and handles OPTIONS preflight automatically.
router.Use(middleware.CORSMiddleware(middleware.CORSOptions{
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,DELETE",
AllowHeaders: "Content-Type,Authorization",
ExposeHeaders: "X-Request-Id",
AllowCredentials: "true",
MaxAge: "86400",
}))Helmet middleware
Sets security-hardening HTTP headers. Use the opinionated defaults or configure individually.
router.Use(middleware.DefaultSecurityMiddleware())
router.Use(middleware.SecurityMiddleware(middleware.SecurityOptions{
ContentSecurityPolicy: "default-src 'self'; img-src *",
XFrameOptions: "SAMEORIGIN",
StrictTransportSecurity: "max-age=31536000",
}))| Header | Default |
|---|---|
| Content-Security-Policy | default-src 'self' |
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
| X-XSS-Protection | 1; mode=block |
| Referrer-Policy | no-referrer |
| Cache-Control | no-store, no-cache, must-revalidate |
JWT Auth middleware
Full JWT authentication with optional refresh token rotation, RBAC, and custom claims validation.
token, _ := middleware.GenerateJWT("secret", jwt.MapClaims{
"user_id": "u-123", "role": "admin",
}, 15*time.Minute, nil)
refresh, _ := middleware.GenerateRefreshToken("refresh-secret",
jwt.MapClaims{"user_id": "u-123"}, 7*24*time.Hour, nil)
router.Use(middleware.JWTAuthMiddleware(middleware.JWTOptions{
AccessSecret: "secret",
RefreshSecret: "refresh-secret",
EnableRefreshToken: true,
RequiredRoles: []string{"admin", "editor"},
UserContextKey: "user",
}))Custom Token Extraction
TokenLookup: func(ctx *breeze.Context) (string, string, error) {
access := ctx.Req.Header["x-access-token"]
refresh := ctx.Req.Header["x-refresh-token"]
if access == "" {
return "", "", fmt.Errorf("token missing")
}
return access, refresh, nil
},Rate Limiter middleware
In-memory token counter per remote IP. Resets after each Per window. Returns 429 Too Many Requests.
router.Use(middleware.NewRateLimiter(middleware.RateLimiterOptions{
Requests: 100,
Per: time.Minute,
}))
router.Handle(breeze.POST, "/login", loginHandler,
middleware.NewRateLimiter(middleware.RateLimiterOptions{
Requests: 5,
Per: time.Minute,
Message: "Too many login attempts",
}),
)ETag Cache middleware
Computes an MD5 ETag from the response body. On matching If-None-Match, returns 304 Not Modified.
cache := middleware.NewETagCache() router.Use(cache.ETagMiddleware())
Compression middleware
Negotiates the best encoding from the client's Accept-Encoding header. Priority: Brotli → Gzip → Deflate → none.
router.Use(middleware.CompressionMiddleware()) // Sets Content-Encoding: br / gzip / deflate automatically
Scalar / OpenAPI 3.1 new
Annotate routes at registration time using Go structs. No YAML, no code generation, no build step.
type CreateUserRequest struct {
Name string `json:"name" description:"Full name" example:"Alice"`
Email string `json:"email" description:"Email address" example:"alice@example.com"`
}
router.Use(middleware.ScalarMiddleware(router, middleware.ScalarOptions{
Title: "Users API",
Version: "2.0.0",
JSONPath: "/Scalar.json",
UIPath: "/Scalar",
}))
router.Handle(breeze.POST, "/users", createUser,
middleware.DocPOST("/users", Scalar.RouteDoc{
Title: "Create user",
Tags: []string{"Users"},
Input: []Scalar.InputGroup{{Type: Scalar.InputBody, Fields: CreateUserRequest{}, Required: true}},
Output: UserResponse{},
OutputStatus: 201,
}),
)| Constant | Source | OpenAPI Location |
|---|---|---|
Scalar.InputBody | JSON request body | requestBody |
Scalar.InputQuery | URL query params | query |
Scalar.InputParams | Path parameters | path |
Scalar.InputHeader | Request headers | header |
WebSocket built-in
Breeze implements RFC 6455 WebSocket directly inside the gnet event loop — no goroutine-per-connection, no third-party WS library. After the HTTP upgrade handshake, the connection is promoted to WebSocket mode and all subsequent traffic bypasses the HTTP parser entirely.
type ChatHandler struct { hub *breeze.WSHub }
func (h *ChatHandler) OnConnect(conn *breeze.WSConn) {
h.hub.BroadcastExcept(breeze.WsOpText, []byte("a user joined"), conn)
}
func (h *ChatHandler) OnMessage(conn *breeze.WSConn, opcode byte, payload []byte) {
if opcode == breeze.WsOpText {
h.hub.BroadcastText(fmt.Sprintf("[%s]: %s", conn.RemoteAddr(), payload))
}
}
func (h *ChatHandler) OnClose(conn *breeze.WSConn, code uint16, reason string) {
h.hub.BroadcastText("a user left")
}
func main() {
router := breeze.NewRouter()
app := breeze.New(router, breeze.NewWorkerPool(runtime.NumCPU()))
chat := &ChatHandler{}
chat.hub = app.WebSocket("/ws", chat)
app.Run(3000, true)
}Handler Interface
type WSHandler interface {
OnConnect(conn *WSConn)
OnMessage(conn *WSConn, opcode byte, payload []byte)
OnClose(conn *WSConn, code uint16, reason string)
}
// Inline shorthand — nil fields are no-ops
app.WebSocket("/ws/echo", &breeze.WSHandlerFunc{
Connect: func(conn *breeze.WSConn) { conn.SendText("ready") },
Message: func(conn *breeze.WSConn, opcode byte, payload []byte) {
conn.Send(opcode, payload)
},
})| Constant | Value | Use |
|---|---|---|
breeze.WsOpText | 0x1 | UTF-8 text message |
breeze.WsOpBinary | 0x2 | Binary message |
WSConn
conn.SendText("hello")
conn.SendBinary([]byte{0x01, 0x02})
conn.Send(breeze.WsOpText, []byte("raw"))
conn.Close(1000, "bye")
addr := conn.RemoteAddr()| Method | Signature | Notes |
|---|---|---|
Send | (opcode byte, payload []byte) error | Low-level send with explicit opcode |
SendText | (msg string) error | Convenience wrapper for text frames |
SendBinary | (msg []byte) error | Convenience wrapper for binary frames |
Close | (code uint16, reason string) | Sends Close frame; idempotent |
RemoteAddr | () string | Client IP:port string |
WSHub
hub := app.Hub()
hub.BroadcastText("server restarting in 10s")
hub.BroadcastBinary(data)
hub.Broadcast(breeze.WsOpText, []byte("raw"))
hub.BroadcastExcept(breeze.WsOpText, []byte(msg), senderConn)
n := hub.Count() // int64 — atomic read
// Multiple routes share the same hub
hub1 := app.WebSocket("/ws/chat", chatHandler)
hub2 := app.WebSocket("/ws/events", eventsHandler)
// hub1 == hub2 == app.Hub()Close Codes
| Code | Name | When |
|---|---|---|
| 1000 | Normal Closure | Clean close initiated by either side |
| 1001 | Going Away | Server shutting down or client navigating away |
| 1002 | Protocol Error | Malformed frame — Breeze closes automatically |
| 1003 | Unsupported Data | Unknown opcode — Breeze closes automatically |
| 1006 | Abnormal Closure | TCP disconnect without Close frame (synthesised) |
| 1008 | Policy Violation | Application-level rejection (use in your handler) |
| 1009 | Message Too Big | Payload exceeds 4 MiB |
| 1011 | Internal Error | Unexpected server-side condition |
gRPC Code Generation new
Breeze's CLI generates gRPC server and client scaffolding directly from a plain Go interface — no .proto file, no naming convention on your methods. Point it at any interface declared in a *_grpc.go file, annotate each method with a grpc_type comment, and run one command.
Convention-free detection
Any interface inside a file matching *_grpc.go is treated as a service definition — no base interface or tag required.
Annotation-driven call style
A grpc_type comment above each method decides whether it's generated as unary, streaming, or bidirectional.
Server, client & adapters
One command generates the server stub, client wrapper, and the adapter glue code that wires the service into your app.
Call Type Annotations
Breeze doesn't infer streaming behaviour from method signatures or naming — it reads an explicit grpc_type comment directly above the method. This keeps generation predictable even as your interface grows.
| grpc_type | Client → Server | Server → Client | Typical use |
|---|---|---|---|
Unary | single request | single response | Standard RPC call — request in, response out. |
ServerSideStreaming | single request | stream of responses | Live feeds, paginated results, subscriptions. |
ClientSideStreaming | stream of requests | single response | Uploads, batched writes, aggregation. |
Bidirectional | stream of requests | stream of responses | Chat, real-time sync, duplex protocols. |
Generating a Service
Define your service as a regular Go interface in a file ending in _grpc.go. Annotate each method with its call type:
// user_grpc.go
package services
// grpc_type: Unary
type UserService interface {
GetUser(ctx context.Context, req *GetUserRequest) (*UserResponse, error)
// grpc_type: ServerSideStreaming
WatchUser(ctx context.Context, req *GetUserRequest, stream UserService_WatchUserServer) error
// grpc_type: ClientSideStreaming
BatchCreateUsers(stream UserService_BatchCreateUsersServer) error
// grpc_type: Bidirectional
SyncUsers(stream UserService_SyncUsersServer) error
}
Then run the generator against the interface name:
breeze generate grpc UserService # overwrite previously generated files for this interface breeze generate grpc UserService --force
| Flag | Description |
|---|---|
--force | Overwrite existing generated files for this interface instead of failing when they already exist. |
Generated Files
Generation is driven by generate_grpc.go (detection and parsing), with output split across a small set of purpose-built files so hand-written code and generated code never collide:
| File | Responsibility |
|---|---|
generate_grpc.go | Scans for *_grpc.go files, parses interfaces and their grpc_type annotations. |
generate_grpc_files.go | Emits the generated server/client Go source files to disk. |
generate_grpc_adapters.go | Generates the adapter glue that wires the service into a Breeze *Router. |
generate_grpc_tags.go | Parses and validates grpc_type tag values. |
Regenerating for the same interface replaces its previously generated block — safe to re-run after adding or changing methods. Pass --force to overwrite files that already exist on disk.
Full Example
A minimal unary service, generated and mounted alongside your normal HTTP routes:
// user_grpc.go
package services
// grpc_type: Unary
type UserService interface {
GetUser(ctx context.Context, req *GetUserRequest) (*UserResponse, error)
}
breeze generate grpc UserService
Mount the generated adapter the same way you'd wire up any other service:
package main
import (
"runtime"
"github.com/nelthaarion/breeze"
"myapp/services"
)
func main() {
router := breeze.NewRouter()
userSvc := services.NewUserServiceServer(&services.UserServiceImpl{})
userSvc.Register(router) // generated adapter wires gRPC handlers in
pool := breeze.NewWorkerPool(runtime.NumCPU())
app := breeze.New(router, pool)
app.Run(3000, true)
}
Note: the constructor / Register call names above follow the same pattern as the existing resource/handler generators (generated file + explicit registration call). Verify the exact generated symbol names against generate_grpc_adapters.go's output before publishing.
Developer Dashboard new
A built-in, production-grade observability surface for every Breeze app — one mount call, zero external dependencies.
The dashboard is shipped as a native Breeze module under github.com/nelthaarion/breeze/dashboard. It mounts a single-page application at /dashboard that gives you live insight into traffic, routes, database queries, caches, queues, scheduler jobs, logs, runtime metrics, and per-request traces — without leaving your process, and without wiring up a second observability stack.
It is engineered to be zero-overhead when disabled. If you never call dash.Mount(), the dashboard contributes no goroutines, no routes, no allocations, and no GC pressure to your hot path. Once enabled, instrumentation uses lock-free atomics and per-event-loop ring buffers so the act of measuring never becomes the bottleneck.
Single-file SPA. The entire UI is served from one in-memory HTML response with inlined CSS and JS — no CDN, no build step, no external fonts. It works on air-gapped networks and behind corporate proxies that strip external assets.
What's inside
Real-time Overview
RPS, latency p50/p95/p99, memory, goroutines, CPU — updated every second.
Routes Explorer
Per-route latency, call counts and error rates with method filtering.
API Explorer
Send test requests and copy ready-to-run snippets in 6 languages.
Live Requests
WebSocket-pushed feed of every incoming request and its outcome.
Database Browser
Read-only, paginated inspection of any registered table.
Query Monitor
Slow-query detection, N+1 warnings and per-query timings.
Cache · Queue · Scheduler
Hit/miss ratios, queue depth, and upcoming job runs at a glance.
Logs
Five-tab log viewer: App, HTTP, Errors, Panics, Warnings.
Health Checks
Green / yellow / red indicators for every registered probe.
Runtime Metrics
GC pauses, heap snapshots and CPU profile, charted live.
Developer Timeline
Per-request profiler with expandable steps for every subsystem.
Auth & Masking
HTTP Basic Auth plus automatic redaction of cookies and API keys.
Setup & Authentication
Mounting the dashboard is a single call. The constructor takes the host *breeze.Router and a Config struct; Mount() registers the /dashboard route tree. Everything else is automatic — instrumentation hooks are wired into the router, the worker pool, and the WebSocket engine as soon as the dashboard is mounted.
package main
import (
"os"
"runtime"
"github.com/nelthaarion/breeze"
"github.com/nelthaarion/breeze/dashboard"
middleware "github.com/nelthaarion/breeze/middlewares"
)
func main() {
router := breeze.NewRouter()
router.Use(middleware.RecoveryMiddleware())
router.Use(middleware.LoggingMiddleware())
// ── Application routes ────────────────────────────────────────────
router.Handle(breeze.GET, "/health", func(ctx *breeze.Context) {
ctx.JSON(map[string]string{"status": "ok"})
})
// ── Mount the developer dashboard ────────────────────────────────
dash := dashboard.New(router, dashboard.Config{
Username: "admin",
Password: os.Getenv("DASH_PASSWORD"), // never hard-code
Path: "/dashboard", // default; shown for clarity
Enabled: true, // omit or false to disable
MaskSecrets: []string{"Authorization", "Cookie", "X-API-Key"},
})
dash.Mount() // registers GET /dashboard and /dashboard/*
pool := breeze.NewWorkerPool(runtime.NumCPU())
app := breeze.New(router, pool)
app.Run(3000, true)
}
Config fields
| Field | Type | Default | Description |
|---|---|---|---|
Username | string | "admin" | HTTP Basic Auth username. Must be non-empty or Mount() panics. |
Password | string | — | HTTP Basic Auth password. Read from an env var or secret manager in production. |
Path | string | "/dashboard" | Root path the SPA is served at. Must start with /. |
Enabled | bool | false | When false, Mount() is a no-op — perfect for disabling in tests via a single env flag. |
MaskSecrets | []string | see note | Header names whose values are redacted in the Live Requests feed and log viewer. Default covers Authorization, Cookie, and X-API-Key. |
DB | *sql.DB | nil | Optional. When set, the Database Browser and ORM Query Monitor become available. |
HealthChecks | []HealthCheck | nil | Application-defined probes (DB ping, cache ping, downstream API) surfaced in the Health tab. |
Zero-overhead when disabled. If Enabled is false, New() still returns a non-nil *Dashboard, but Mount() short-circuits before registering any routes or starting any goroutines. This means you can ship the same binary to production and to your test harness without paying any instrumentation cost in CI.
Authentication model
The dashboard is protected by HTTP Basic Auth over the same TLS (or plain HTTP) connection as your app. Credentials are compared in constant time using crypto/subtle to mitigate timing attacks. On a failed challenge the dashboard responds with 401 WWW-Authenticate: Basic realm="breeze-dashboard" — browsers will then prompt the user natively.
For multi-tenant deployments or stricter access control, mount the dashboard behind an additional middleware: router.Use(middleware.JWTAuthMiddleware(secret)) before calling dash.Mount(). JWT and Basic Auth compose cleanly because they live in different layers of the request pipeline.
Real-time Overview
The landing panel of the dashboard. It gives you a one-glance answer to "is my service healthy right now?" by combining five high-signal metrics, each updated on a one-second cadence via a single multiplexed WebSocket.
| Metric | Source | What it tells you |
|---|---|---|
| RPS | per-event-loop request counter | Sustained throughput. Sudden drops usually mean an upstream dependency or DNS issue. |
| Latency p50 / p95 / p99 | per-route histogram, ring-buffered | Where most users live (p50) vs. the long tail (p99). A widening p99/p50 ratio is the earliest sign of contention. |
| Memory | runtime.ReadMemStats | Heap alloc, in-use heap and total alloc rate. Sawtooth patterns indicate healthy GC; flat-up ramps indicate a leak. |
| Goroutines | runtime.NumGoroutine | Active goroutine count. A monotonic climb without a corresponding traffic increase is the classic leak signature. |
| CPU | per-event-loop utilisation | How saturated each reactor is. With multiCore=true, you'll see one trace per event loop. |
Charts are rendered client-side using a tiny inlined SVG library — no charting framework is shipped, which keeps the single-file payload under 80 KB gzipped. Each series can be toggled, pinned to a custom range, or exported as CSV via the toolbar.
Backfill on connect. When the WebSocket opens, the server immediately pushes the last 60 seconds of every series so charts are populated on first paint — no empty-graph flash.
Routes Explorer
A live table of every route registered on the host *breeze.Router, joined with per-route runtime statistics. The route table itself comes from Router.RoutesInfo() — the same read-only view the framework exposes to external packages — so what you see in the dashboard is always byte-for-byte identical with what the router will actually match.
| Column | Meaning |
|---|---|
Method · Pattern | HTTP method and route pattern, with :param and *wildcard segments highlighted |
Calls | Total invocations since process start |
p50 / p95 / p99 | Latency quantiles, computed from a 5-minute sliding window |
Errors | Responses with status ≥ 500, with a percentage of total calls |
Last | Time since the most recent invocation |
Click any row to drill into a per-route detail view: the middleware chain (in execution order), the handler source location if available, the recent request samples, and a latency distribution histogram. Filtering by method or by a free-text pattern match is supported from the toolbar.
Tip
Sort by p99 descending to instantly find the slowest endpoints in your service — this is usually more actionable than overall p99 because it isolates the few routes that drag the aggregate up.
API Explorer
A built-in alternative to Postman or Scalar UI. Pick any route from the Routes Explorer, fill in path params, query string, headers and a JSON body, and click Send. The dashboard issues the request through the same gnet event loop your real traffic uses, so what you measure is what your users get.
Every sent request can be exported as a ready-to-run code snippet in six languages. Use the language tabs above the response panel.
curl -X POST https://api.example.com/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Ada","email":"ada@lovelace.dev"}'req, _ := http.NewRequest("POST", "https://api.example.com/users",
strings.NewReader(`{"name":"Ada","email":"ada@lovelace.dev"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Ada", email: "ada@lovelace.dev" }),
});import requests
requests.post(
"https://api.example.com/users",
headers={"Authorization": f"Bearer {token}"},
json={"name": "Ada", "email": "ada@lovelace.dev"},
)using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", token);
var resp = await http.PostAsJsonAsync(
"https://api.example.com/users",
new { name = "Ada", email = "ada@lovelace.dev" });$ch = curl_init("https://api.example.com/users");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"name" => "Ada", "email" => "ada@lovelace.dev",
]),
]);
curl_exec($ch);Responses are pretty-printed, syntax-highlighted, and timed. The status, latency and response size are surfaced in the toolbar so you can compare against the route's historical p95 in one glance.
Live Requests Feed
A real-time, tail-style feed of every request handled by the framework. Push happens over a dedicated WebSocket channel — the dashboard multiplexes all of its live views (Overview, Live Requests, Logs, Timeline) over a single socket to keep connection overhead predictable.
Each entry shows the method, path, status, latency, and the originating client IP. Click an entry to expand the full request/response envelope: headers (with secrets masked — see Setup & Authentication), request body, response body, and a deep-link into the Developer Timeline for that request.
| Control | Behaviour |
|---|---|
| Pause | Stops rendering new entries without closing the socket; existing entries remain visible |
| Filter | Free-text match on method, path or status code; supports status:>=400 and method:GET prefixes |
| Clear | Drops the in-browser buffer only — server-side ring buffer is unaffected |
| Replay | Re-issues the selected request through the API Explorer, pre-filled with the original headers and body |
Memory-bounded. The server keeps the last 1,024 request envelopes per event loop in a ring buffer; older entries are evicted automatically. The browser keeps the last 200 visible entries by default — bump it from the toolbar if you need more history in a tab.
Database Browser
A read-only, paginated table inspector for the database registered via Config.DB. Use it to verify schema state, look up a row by primary key, or sanity-check reference data during development — without reaching for a separate SQL client.
Capabilities
- List all tables and views exposed by the
*sql.DBdriver - Inspect column names, types, nullability and primary-key flags
- Paginated row view (50 rows/page, configurable) with column sorting
- Free-text
WHEREclause builder for ad-hoc filtering - CSV export of the current result set, capped at 10,000 rows
Read-only by construction. The browser only ever issues SELECT statements, and it wraps every query in a read-only transaction (sql.TxOptions{ReadOnly: true}). Drivers that respect this flag (Postgres, MySQL with --read-only) will reject any write attempt at the engine level — there is no UI path that can mutate data.
Production safety
The browser is disabled by default in production builds. Enable it explicitly via Config.EnableDBBrowser, and consider pointing it at a read-replica instead of your primary. The dashboard will refuse to mount the Database Browser if Config.DB is nil, so a misconfiguration fails closed rather than silently serving an empty page.
ORM Query Monitor
When a Breeze ORM adapter is registered with the dashboard, every SQL statement it issues is captured with its wall-clock duration, the calling goroutine, and — when source information is available — the file and line that triggered it. The result is a per-query timeline that makes slow queries and N+1 patterns obvious without a separate APM agent.
| Signal | Detection rule | Severity |
|---|---|---|
| Slow query | Wall time exceeds the configured threshold (default 200 ms) | Warning |
| N+1 pattern | The same parameterised query runs more than 5 times within a single request | Warning |
| Missing index | EXPLAIN indicates a sequential scan on a table larger than 10k rows | Info |
| Idle in transaction | A transaction stays open longer than 1 s without a follow-up query | Error |
| High statement churn | Distinct prepared-statement count climbs without bound | Info |
Each finding links back to the Developer Timeline entry that produced it, so you can see the exact request, the exact handler, and the exact line of code that triggered the offending query — closing the loop from symptom to root cause in a single click.
// Registering the ORM adapter wires query capture into the dashboard.
// Replace with your actual adapter (sqlx, gorm, ent, …).
dash.RegisterORM(breezeorm.NewAdapter(db, breezeorm.Config{
SlowThreshold: 200 * time.Millisecond,
NPlusOneThreshold: 5,
CaptureExplain: true,
}))
Cache, Queue & Scheduler Monitors
Three side-by-side panels summarise the stateful subsystems that often fail silently: caches, queues, and schedulers. Each monitor pulls from a small adapter interface — implement it once per backend (Redis, in-memory, Asynq, River, cron, …) and the dashboard handles the rest.
Cache panel
- Hit / miss / error ratio per cache, with a 60-second rolling sparkline
- Current key count, total bytes, and eviction rate
- Top-10 keys by access frequency — useful for spotting hot keys
Queue panel
- Depth and throughput per queue, with consumer-lag trend
- Dead-letter count and oldest unacked message age
- Per-worker goroutine state (idle / busy / crashed)
Scheduler panel
- Job name, cron expression, next run, last run, last status
- Last-run duration vs. the job's interval — flag jobs that overlap themselves
- Manual "Run now" trigger for one-off execution during debugging
Zero-config for in-process backends. If you use Breeze's built-in in-memory cache and worker-pool-backed scheduler, the adapters are registered automatically when dash.Mount() runs. External backends (Redis, Asynq, etc.) require an explicit dash.RegisterCache(...) / RegisterQueue(...) / RegisterScheduler(...) call.
Logs
A unified log viewer with five tabs, each backed by an independent ring buffer so opening one tab does not evict entries from another. Logs are streamed live over the same multiplexed WebSocket as the rest of the dashboard, with backpressure handled by dropping the oldest entries rather than blocking the application.
| Tab | Source | Default buffer |
|---|---|---|
| App | log.Printf / breeze.Log output captured via log.SetOutput | 2,048 lines |
| HTTP | One entry per handled request, written by the LoggingMiddleware | 4,096 lines |
| Errors | Application-returned responses with status ≥ 500 | 1,024 lines |
| Panics | Recovered panics from RecoveryMiddleware or the worker pool, with stack traces | 512 lines |
| Warnings | Slow queries, N+1 detections, idle-in-transaction, etc. | 1,024 lines |
Every tab supports case-insensitive free-text filtering, level filtering (where applicable), and a "follow" toggle that auto-scrolls to the newest entry. Long lines are truncated with a click-to-expand affordance so the table stays scannable.
Secrets never leave the process. The MaskSecrets list from Config is applied before any line is written to a ring buffer. A redacted header value looks like Authorization: <redacted 28 chars> in the UI — enough to confirm presence and length, never enough to leak.
Health Checks & Runtime Metrics
Health checks
Each HealthCheck registered via Config.HealthChecks is polled on a configurable interval (default 15 s). The result is rendered as a coloured chip — green (pass), yellow (slow > 1 s), red (fail) — alongside the last latency, last error message, and a 60-sample sparkline of recent latencies.
dash := dashboard.New(router, dashboard.Config{
// …
HealthChecks: []dashboard.HealthCheck{
{Name: "postgres", Check: func(ctx context.Context) error {
return db.PingContext(ctx)
}},
{Name: "redis", Check: func(ctx context.Context) error {
return rdb.Ping(ctx).Err()
}},
{Name: "downstream:billing", Check: func(ctx context.Context) error {
return billingClient.Health(ctx)
}},
},
HealthInterval: 15 * time.Second,
})
Go runtime metrics
A dedicated panel surfaces the runtime statistics that matter most for production Go services. All numbers are sampled in-process via runtime/metrics and runtime/pprof — no agent, no sidecar, no OpenTelemetry SDK required.
| Metric | Visualization | Why it matters |
|---|---|---|
| GC pause time | p50 / p95 / max line chart | Tail latency spikes almost always correlate with GC pauses above 10 ms |
| Heap allocations / sec | area chart | The single best leading indicator of GC pressure |
| Live heap bytes | line chart | Sawtooth = healthy; monotonic ramp = leak |
| Goroutine count | line chart | Unbounded growth without traffic growth = goroutine leak |
| CPU profile | flame graph (truncated to top 30 frames) | On-demand 5-second CPU profile, rendered in-browser |
| Memory profile | flame graph | On-demand heap profile, useful for leak hunting |
CPU and memory profiles are captured on demand — click the Capture profile button, wait 5 seconds, and the flame graph renders inline. Profiles never touch disk; they are decoded entirely in the browser.
Developer Timeline
The flagship debugging view. Each handled request is captured as a hierarchical timeline of spans — one per middleware, one per DB query, one per cache call, one per downstream HTTP call. The result is a flame-style chart that shows where the request spent its time, with every leaf expandable to the underlying event.
Span types captured
Anatomy of a timeline entry
Each entry expands to show:
- Start offset — milliseconds since request arrival
- Duration — wall time, with a bar proportional to the slowest sibling
- Args — query text, cache key, outbound URL, etc. (truncated, click to expand)
- Result — rows affected, cache hit/miss, HTTP status, error if any
- Source — file:line of the call site, when debug info is enabled
Click any span to filter the timeline to siblings of the same type — instantly answer questions like "show me every DB query this request made, in order" without scrolling past middleware noise.
Always-on, low-cost. Spans are recorded into a per-request slice backed by a sync.Pool-managed buffer; in benchmarks the overhead is < 1% on the hot path. You can leave the timeline enabled in production without measurable impact, and sample only the requests you actually want to inspect via Config.TimelineSampling.
Workflow tip
When a user reports a slow request, grab the request ID from your access logs, paste it into the timeline filter, and you'll see the exact span breakdown for that single request — no need to reproduce, no need to re-deploy with extra logging.
File Uploads
files, fields, err := ctx.ParseMultipart(10 << 20) // 10 MB max
if err != nil { ctx.Status(400); ctx.WriteString(err.Error()); return }
uf := files["avatar"][0]
fmt.Println(uf.Filename, uf.ContentType, uf.Size)
saved, err := ctx.SaveUploadedFile("avatar", "./uploads/avatar.jpg", 5<<20)
if err != nil { ctx.Status(400); ctx.WriteString(err.Error()); return }
ctx.JSON(map[string]string{"saved": saved})| Field | Type | Description |
|---|---|---|
Field | string | Form field name |
Filename | string | Original filename from the browser |
ContentType | string | From Content-Type or auto-sniffed |
Size | int64 | Size in bytes |
Content | []byte | Raw file bytes |
Static Files
// Serve ./public/* at /assets/*
router.ServeStatic("/assets", "./public")
// Auto-serve ./public/index.html at GET / (no explicit route needed)
// Directory traversal is sanitized with filepath.CleanWorker Pool
Off-loads handler execution from the gnet event loop so slow handlers don't block all connections on a reactor.
pool := breeze.NewWorkerPool(runtime.NumCPU()) pool := breeze.NewWorkerPool(32) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() pool.Shutdown(ctx) // Pass nil to skip the pool and use raw goroutines app := breeze.New(router, nil)
| Scenario | Behaviour |
|---|---|
| Queue has capacity | Task enqueued normally (channel buffer = workers × 16) |
| Queue full (burst) | Falls back to go task() — event loop is never blocked |
| pool is nil | Uses go exec() per request |