App
doors.App is the HTTP entry point for your Doors application.
Most apps follow three small steps:
- create the app with
doors.NewApp(page, options...) - attach middleware with
app.Use(...)for static files, logging, CORS, or anything else that runs before request handling - pass the app to
http.ListenAndServe
app := doors.NewApp(func(ctx context.Context, r doors.Request) gox.Comp {
return App{}
})
app.Use(
doors.UseDir("/assets/", "./assets", doors.CacheControlStatic),
)
if err := http.ListenAndServe(":8080", app); err != nil {
panic(err)
}
doors.App implements http.Handler, so it plugs straight into the standard Go server, a chi/mux parent, or any other HTTP setup.
In practice, the app pulls together:
- middleware that runs in front of all request handling (static files, logging, CORS, ...)
- app-wide settings (CSP, esbuild, server ID, error pages, session tracking, system limits)
- where the binary plugs into your HTTP setup
Page Factory
The function passed to doors.NewApp(...) is a per-request page factory, not a router. It runs once each time a fresh page instance starts (a navigation, a reload, an open-in-new-tab) and returns the root component for that instance.
app := doors.NewApp(func(ctx context.Context, r doors.Request) gox.Comp {
auth := doors.SessionStore(ctx).Init(authKey{}, func() any {
c, err := r.GetCookie("session")
if err != nil {
return doors.NewSource(false)
}
_, ok := store.Get(c.Value)
return doors.NewSource(ok)
}).(doors.Source[bool])
return App{auth: auth}
})
Most apps return the same root component every time — what changes per request is the state you attach to it. The factory exists so you can:
- bootstrap session-scoped state from cookies or headers (auth, theme, locale) before the page renders
- read or write the location for this instance
- inspect request headers, set cookies, set response headers
- decide which component to return when there are very few top-level shapes (e.g. an error page in odd cases) — but day-to-day routing is not this; routing happens inside the returned component (see Routing below).
For the auth-bootstrap pattern in full, see Storage & Auth.
The function receives:
ctx— the Doors runtime context for the new instance. Pass this to Doors APIs.r doors.Request— request/response headers, cookies, and the underlying HTTP context (r.Context()).
Configuration Options
App-wide settings are passed as doors.With... options to NewApp:
app := doors.NewApp(page,
doors.WithConf(doors.Conf{
RequestTimeout: 20 * time.Second,
}),
doors.WithCSP(doors.CSP{
ConnectSources: []string{"https://api.example.com"},
}),
)
See Configuration for the full list.
Middleware
app.Use(...) adds standard func(http.Handler) http.Handler middleware in front of all handlers, including system endpoints under /~/.... It can short-circuit static files, set headers, gate access, log, or hand off to another mux before Doors handles a request. Middleware runs after internal session initiation.
UseFS
Serve an embedded fs.FS (or any fs.FS) under a URL prefix:
//go:embed assets/*
var assetsFS embed.FS
app.Use(
doors.UseFS("/assets/", assetsFS, doors.CacheControlImmutable),
)
UseDir
Serve a directory from disk under a URL prefix:
app.Use(
doors.UseDir("/public/", "./public", doors.CacheControlStatic),
)
UseFile
Serve a single file at a fixed path:
app.Use(
doors.UseFile("/robots.txt", "./static/robots.txt", doors.CacheControlStatic),
)
UseResource
doors.UseResource exposes a Doors static resource at a fixed public path. This goes through the resource registry and gets caching, gzip, and CSP integration:
app.Use(
doors.UseResource(
"/assets/sans.ttf",
doors.ResourceFS(assetsFS, "sans.ttf"),
"font/ttf",
),
)
The contentType argument is optional — pass an empty string to let the registry detect it.
Cache-Control Presets
The middleware helpers take a cacheControl string. Common presets are exported as constants:
| Constant | Value | Use for |
|---|---|---|
doors.CacheControlImmutable |
public, max-age=31536000, immutable |
Fingerprinted assets that never change at a URL |
doors.CacheControlStatic |
public, max-age=3600, must-revalidate |
Long-lived but non-fingerprinted files (/favicon.ico, /robots.txt) |
doors.CacheControlStaticShort |
public, max-age=300, must-revalidate |
Static files that may change occasionally |
doors.CacheControlHTML |
public, max-age=0, must-revalidate |
HTML entry points (pair with an ETag) |
doors.CacheControlCDN |
public, max-age=3600, s-maxage=86400 |
Browser short, CDN longer |
doors.CacheControlPrivate |
private, max-age=0, must-revalidate |
Per-user responses |
doors.CacheControlNoCache |
no-cache |
Always revalidate |
doors.CacheControlNoStore |
no-store |
Sensitive responses |
doors.CacheControlAPI |
private, no-cache |
JSON APIs |
Pass an empty string when you don't want Doors to set a Cache-Control header.
Custom Middleware
app.Use(...) accepts any func(http.Handler) http.Handler, so existing Go middleware composes naturally:
app.Use(
logger.Handler,
cors.New(cors.Options{...}).Handler,
doors.UseDir("/assets/", "./assets", doors.CacheControlImmutable),
)
Middleware runs in registration order. Anything that doesn't short-circuit falls through to the Doors handler (system endpoints or page rendering).
Mounting Inside Another Server
doors.App is a regular http.Handler, so you can mount it inside any router:
mux := http.NewServeMux()
mux.Handle("/admin/", adminHandler)
mux.Handle("/", app)
http.ListenAndServe(":8080", mux)
For request matching, Doors uses the URL it sees, so mount it at / unless you also configure your model paths accordingly.
Session & Instance Count
app.InstanceCount() returns the total number of live page instances across all sessions. Each open tab or window in a browser counts as one instance.
n := app.InstanceCount()
app.SessionCount() returns the total number of active sessions.
n := app.SessionCount()
Both are useful for monitoring, health checks, and diagnostics — for example, exposing the values through a /metrics endpoint or logging them periodically.
Drain Mode
app.Drain(callback) switches the app into one-way drain mode. It is meant for rollouts where an old process should keep existing interactive pages alive, but move users to a newer process as soon as they perform normal navigation.
While draining, existing instances still handle their current page and server-side events. When a live instance performs a Doors link navigation, or when the browser asks Doors to restore a previous location, Doors ends that old instance and tells the browser to load the target URL as a normal page request. If your proxy or load balancer now routes normal page requests to the new deployment, the user lands on the new process.
app.Drain(func() {
if err := server.Shutdown(context.Background()); err != nil {
logger.Error("server shutdown failed", "error", err)
}
})
The callback runs once, after app.InstanceCount() reaches zero. If there are no live instances when Drain is called, the callback runs immediately. Later Drain calls are ignored and logged as errors.
Drain does not stop the HTTP server, reject new page loads, or change proxy routing by itself. For a zero-downtime rollout, first arrange for new full page requests to stop reaching the old process, then call Drain on the old app.
Routing
doors.NewApp itself does not match URLs. The page function returns a single root component for every request, and routing happens inside that component using doors.Route(...):
elem (a App) Main() {
<!doctype html>
<html lang="en">
<body>
~(doors.Route(
doors.RouteModel(elem(p doors.Source[Path]) {
~Page{path: p}
}),
doors.RouteLocationDefaultComp(NotFound{}),
))
</body>
</html>
}
Path models, route builders, default fallbacks, and routing on derived state are covered in Routing.