The httpd subpackage provides a high-level, "batteries-included" HTTP server implementation that implements the webtyp.com/router contract.
The development mode's internalStrategy (in-memory server) consumes the httpd.NewRouter adapter to provide consistent routing behavior between development and production.
A project does not contain an HTTP main(). maingen.go writes one to
.build/server/main.go — inside the project, because outside the module tree
the Go toolchain cannot resolve the project's own imports — and the External
strategy compiles and runs it.
The generated main imports the project's routes package and calls
routes.Register on the server it builds, so development and production serve
the same route set from the same source. The previous
templates/server_basic.md scaffold is gone: it produced a committed
web/server.go whose routes had to be kept in sync by hand with the ones the
dev process registered, and they drifted.
Which strategy runs is decided by one file: a project with routes/routes.go
gets the compiled server; a project without one gets the in-memory asset server
and compiles nothing. A web/server.go that a developer writes themselves still
wins — the tool never overrides code the user owns — but nothing generates it.
adapter.go: Implementation ofrouter.Router,router.Context, etc., mapping them tonet/http.httpdproducesrouter.ContextKeyRemoteAddrfromRequest.RemoteAddr.middleware.go: Built-inGzipandNoCachemiddlewares.static.go: Static file serving fromPublicDir. Each file carriesCache-Controlfrompwa.CacheControl(content-hashed names immutable,/artifacts/never cached, everything else revalidated) unlessNoCacheis on.enforce.go: RBAC enforcement based onRequiresmetadata.tls.go/localcert.go/ca.go: AutoCert (Let's Encrypt), custom Cert/Key, PlainHTTP, and the local CA default (LocalCA).spki.go:LocalCertSPKI(), the pin the development browser trusts.routes_endpoint.go: Optional JSON endpoint at/_routeslisting all registered routes.httpd.go: CoreServerorchestrator.
- Pattern Matching & Extraction: Delegated to
net/http.ServeMux. Routes registered as"GET /api/items/{id}"natively match parameters in Go 1.22+, accessible viactx.Param(name). - Pattern Validation: Delegated to
webtyp/routerviarouter.ValidatePattern. This server rejects patterns with wildcards (e.g.,{name...}) at registration time so that patterns unsupported by edge runtimes fail early on the dev server. - Introspection Endpoint: Delegated to
router.MountIntrospectionatrouter.IntrospectionPath(/_routes). This repo supplies theConfig.RoutesEndpointboolean toggle and theConfig.Policydescriber.
- Simple Entry Point:
httpd.New(config).Mount(modules).ListenAndServe()is the only way to start the server. - Standard Library Based: Uses
net/httpinternally but doesn't expose it in the public API. - Fails Fast: Validates configuration (like TLS modes or RBAC requirements) at startup rather than at runtime.
The server follows a secure-by-default model where routes are private unless explicitly marked as Public(). However, for a smooth development experience:
- Static Assets: Files served from
PublicDir(e.g., WASM binaries, JS, CSS) are always public. They bypass the RBAC middleware. - Root Path (
/) in Development: The internal strategy registers a default public route for/that servesPublicDir/index.htmlor a diagnostic message. This ensures that the frontend application is accessible immediately without manual RBAC configuration.
The zero value of TLSConfig serves HTTPS with the local certificate authority in development and production. Setting PlainHTTP: true is the only way to serve plain HTTP (e.g. when a proxy in front terminates TLS). A page served by httpd is always a secure context, on localhost and on a LAN.
The CA route (/__webtyp/ca) is mounted automatically by httpd whenever it serves the local CA.
It generates a two-level chain, the same shape mkcert and Caddy's internal PKI use:
- The CA (
httpd/localcert.go,ensureLocalCA) —IsCA: true,KeyUsageCertSign | KeyUsageCRLSign,MaxPathLen: 0, no subject alternative names, long-lived. This is the file a device installs to trust the server. - The leaf (
httpd/localcert.go,ensureLocalCert) — signed by the CA,IsCA: false,serverAuth, and a SAN set coveringlocalhost, the lowercased host computer name,127.0.0.1,::1and every non-loopback IPv4 of the host's interfaces. It is regenerated when that address set changes or when machine name changes, and renews itself automatically 30 days before expiry without requiring a server restart.
A single self-signed certificate cannot do this job: a leaf has CA:FALSE, and
iOS and Android refuse to use it as a trust anchor — the device shows no error,
the page simply keeps failing after the user has followed every instruction.
| Client | Mechanism | Trusts |
|---|---|---|
| The development browser | --ignore-certificate-errors-spki-list with LocalCertSPKI() |
exactly the leaf's public key |
| A phone on the LAN | installs the CA from CAPath (/__webtyp/ca) |
anything the CA signs |
LocalCertSPKI() returns the leaf's SPKI hash, never the CA's. Chrome matches
the flag against any certificate in the presented chain, so pinning the CA would
appear to work — and would silently grant the browser trust in every certificate
that CA ever signs, surviving a leaf rotation unnoticed.
On first generation the CA is also installed into the host's trust stores via
github.com/smallstep/truststore, so that clients which were not launched by
webtyp.com/devbrowser — a second browser, curl, a native client — do not see
a warning. The step is best-effort: on Linux it shells out through sudo and may
prompt, and any failure is logged as a warning rather than aborting the listen.
Setting WEBTYP_LOCALCERT_SKIP_TRUSTSTORE to any non-empty value skips it
entirely; test runners and CI set it. Nothing about serving TLS depends on it —
the SPKI flag and CAPath still work when it is skipped or fails.
Why that dependency rather than mkcert or hand-written per-platform code is
argued in DESIGN.md.
On iOS, installing a profile is two steps and the platform announces only the first: install it (Settings → Profile Downloaded), then enable it under Settings → General → About → Certificate Trust Settings. A profile installed but not trusted behaves exactly like no profile at all.
Windows: open the downloaded file and install it in "Trusted Root Certification Authorities".
Long-lived connections (such as SSE streams or WebSockets registered via r.Stream(...)) have specialized transport requirements:
- WriteTimeout Bypass: Go's
http.Server.WriteTimeoutis 15s by default. Inhttpd, stream handlers invokehttp.NewResponseController(w).SetWriteDeadline(time.Time{})to disable this timeout for the stream's lifetime, preventing abrupt connection terminations. - Hop-by-Hop Header Sanitization: In HTTP/2 (RFC 9113 §8.2.2), connection-specific headers (
Connection,Keep-Alive,Proxy-Connection,Transfer-Encoding,Upgrade) are strictly prohibited and causePROTOCOL_ERROR.httpdautomatically filters these headers whenr.ProtoMajor >= 2. - Gzip Bypass: Any response with
Content-Type: text/event-streambypasses gzip compression to prevent buffering delays and HTTP/2 framing issues.