This file explains why parts of webtyp/server are built the way they are,
including the options that were considered and dropped. It is the place to look
when you read the code, think "this could have been done more simply," and want
to know whether that was already tried.
It is a companion to ARCHITECTURE.md, which describes what
the pieces are and how they fit together. This one carries the arguments, so that
document can stay short.
A service worker, the Origin Private File System and navigator.storage.persist() exist only in a secure context (HTTPS, or localhost). Previously httpd.Config with a zero TLS field served plain HTTP. A clinic opening its own server as http://192.168.1.20:8080 got a page with no service worker and no OPFS. The fix is that the zero value is HTTPS, using the local certificate authority (LocalCA). Plain HTTP becomes an explicit opt-out (PlainHTTP: true).
Prior art comparison:
- Caddy serves HTTPS by default for every site; for hosts that cannot get a public certificate (
localhost, IPs, internal names) it uses its internal CA, installs it in the system trust store, and HTTP only happens when the address is written withhttp://explicitly. - mkcert uses the same two-level chain but is a separate tool the developer runs by hand; we generate in-process automatically.
- Go
net/httpand most frameworks default to plain HTTP and leave TLS to a proxy. That default is what produced clinic servers without a secure context; we follow Caddy.
Start a WebTyp app in development:
webtyp devA browser opens on https://localhost:8080 — note the s — and shows the
page with no security warning, as if the site had a certificate bought from a
real provider.
That is unusual, and it is the thing this section explains. Two questions hide behind it:
- Why HTTPS at all in development? Because serving plain HTTP locally and
HTTPS in production hides bugs that only surface after deploying —
Securecookies that never get sent,SameSite=None, HSTS, mixed content — and because a phone on the same Wi-Fi cannot install a PWA from anhttp://address. The full reasoning lives in ARCHITECTURE.md → TLS: HTTPS by default. - Why no warning? Normally a certificate a program made for itself produces
"Your connection is not private" in every browser. Avoiding that is what
brought the
github.com/smallstep/truststoredependency into../go.mod, and this section justifies that choice.
- Certificate — a file the server shows the browser saying "I am
localhost," signed by somebody else so it cannot be forged. - Certificate Authority (CA) — whoever does that signing. Public websites pay a commercial CA; in development the server invents its own, a one-off CA that exists only on your machine.
- Trust store — the list of CAs your operating system believes. Firefox, Chrome, macOS, Windows and Linux each keep one, in different formats and different places. A certificate signed by a CA that is not in that list is what triggers the browser warning.
So the warning disappears only if the dev CA is added to that list. That single sentence is the whole problem this dependency solves.
httpd builds the certificates itself, with nothing but crypto/x509 from the
standard library, all of it in
../httpd/localcert.go: a CA called WebTyp Local CA, and a
server certificate signed by it whose list of valid addresses covers localhost,
the computer host name, loopback addresses and every LAN address the machine currently has. Both land
in ~/.webtyp/httpd/certs.
The standard library stops there. Adding a CA to the operating system's trust store is not a Go problem — it is five different problems, one per platform:
| Platform | What has to happen |
|---|---|
| macOS | run security add-trusted-cert |
| Linux | drop the file in the system anchors and run update-ca-certificates or trust anchor |
| Firefox & Chrome on Linux | separately, edit each NSS database with certutil |
| Windows | call the CertAddEncodedCertificateToStore syscall |
| Java apps | add it to the JVM keystore |
That list, not certificate generation, is the gap a dependency is being paid to fill.
mkcert is the well-known tool for
exactly this, and it is the first thing anyone suggests.
truststore is the library that
Smallstep (the authors of step-ca) produced by lifting mkcert's
truststore_darwin.go / _linux.go / _windows.go / _nss.go / _java.go
files out of it and publishing them as an importable module. Its README says so
directly: "Based on https://github.com/FiloSottile/mkcert."
So the per-platform logic in the table above is the same code in both. What differs is the shape it is delivered in:
FiloSottile/mkcert |
smallstep/truststore |
|
|---|---|---|
| Kind | a command-line program you run | a library you import |
| Go package | package main |
package truststore |
| Does | generate a CA, generate certificates, install them, print a friendly CLI | only install/uninstall a certificate in the trust stores |
| Usable from Go code | No | Yes — Install, Uninstall, InstallFile, plus options like WithJava(), WithFirefox(), WithNoSystem() |
| Non-stdlib dependencies | its own CLI stack | howett.net/plist, and nothing else |
| License | BSD-3 | Apache-2.0 |
-
mkcert cannot be imported. Everything in it lives in
package main, which in Go means no other program can call into it. Using it would mean running it as a subprocess —exec.Command("mkcert", ...)— which requires the developer to have installed mkcert first through Homebrew, apt orgo install, plus code to parse its output and to handle the "it is not installed" case. That destroys the property thatwebtyp devis one binary that needs no other tooling on the machine. -
Only a fifth of mkcert is wanted.
httpdalready mints its own chain, in its own directory, with its own naming, and with a list of addresses it recomputes when the laptop changes network. It also publishes the server certificate's public-key fingerprint for the browser launch flag and serves the CA file atCAPathso a phone can install it. mkcert has its own opinions about all of that; adopting it would mean fighting it, not reusing it. The one piece genuinely missing is "install this CA," and that is the entirety of whattruststoredoes — the codebase calls exactly one function from it,truststore.Install. -
Writing it again is not worth it. It is on the order of a thousand lines of per-platform code — NSS databases, the Java keystore, Windows syscalls — that mkcert has been proving in the field since 2018. Nothing about WebTyp would be better for having its own copy.
truststoreis a quiet project (v0.13.0, infrequent releases). The exposure is one function call against an API with no reason to change, and the module is five files under Apache-2.0 — if it were ever abandoned, vendoring it is a small, available exit.- On Linux the install runs
sudointernally and may ask for your password the first time. Because of that the step is best-effort: a failure is logged as a warning and the server keeps going. Setting the environment variableWEBTYP_LOCALCERT_SKIP_TRUSTSTOREto any value skips it altogether — the test runner and CI set it, sogotestnever stops waiting for a password prompt. TLS itself never depends on this step.
- Run
mkcertas a subprocess. Adds a prerequisite the developer must install by hand, and hands control of the CA name, the certificate directory and the address list to a tool that has its own conventions. - Implement trust-store installation inside WebTyp. A large, platform-specific surface to maintain, with no advantage over the existing implementation.
- Never touch the trust store at all. This already exists as the fallback
path: the browser WebTyp launches is told to accept this one certificate, and
any other device can install the CA by hand from
CAPath. It works — but every client WebTyp did not launch itself (a second browser,curl, a native app) goes back to showing the warning. Removing that friction is precisely what the dependency buys.