One module · HTTPS by default · No config language
A production server small enough to read.
Servette is a production nanoserver: a single readable Python module that serves a static site on the public internet with a trusted certificate that renews itself, security headers on every response, rate limiting, and a hardened service that survives reboots.
No configuration language to learn, and a single dependency the install brings with it. Install the package, run the wizard, done.
pipx install servette
# then run: servette
servette> setup Site folder created, empty for now Domain example.com Email for certs you@example.com Password skipped Ready to start? yes ✓ certificate issued (Let's Encrypt) ✓ HTTP → HTTPS redirect active ✓ security headers enabled ✓ installed as a system service Your site is live at https://example.com Renewal is automatic. Nothing else to do.
1
module to read — the whole server, and nothing else
1
third-party dependency — cryptography, installed with the package
6,816
lines of Python — small enough for one person to fully understand
0
config files to write; a wizard asks, then it runs
What it is
A production layer over the smallest possible server.
Python's standard library ships http.server: the canonical nanoserver. It serves a folder in one command, and — by its own documentation — stops well short of production. Servette is the layer that closes that gap, and nothing beyond it.
It gets a real certificate
A trusted Let's Encrypt certificate, issued by Servette's own ACME client during setup and renewed by a watchdog well before it expires. Plain HTTP is redirected up to HTTPS.
It is hard to abuse
Security headers on every response, a sliding-window rate limiter, optional password protection, path-traversal refusal, and a connection cap that closes rather than queues.
It stays up
Installed as a sandboxed systemd service running as its own unprivileged user, restarted within seconds if it dies, and back after a reboot.
It stays legible
One module, small enough for one person to fully understand — a weekend's honest work, not a career. No plugin system, no configuration language, no hidden machinery to take on trust.
Everything in that list exists because it is necessary to serve a site on the open internet honestly. Nothing is there because another server has it.
What it is not
The honest limits.
Servette serves files. Anything that runs in a browser is fair game — a portfolio, a documentation site, a serious client-side app. Anything that needs the server to think is not.
No dynamic content
A POST gets a 405. A POST needs a destination — a database, an email, a file — and Servette has none. A form's backend lives elsewhere.
No SPA deep links
Files are served as they are, so /about 404s unless that file exists. Use hash routing, or a platform with rewrite rules.
No proxying or plugins
No reverse proxy, load balancer, or live config API. Servette can sit behind one trusted proxy hop; it does not become one.
A request to add any of these is not a feature request; it is a request for a different program. The honest answer is usually to reach for a general-purpose server that does more.
This site
Where to go from here
Three pages, in the order most people want them.
How it Compares
Where Servette sits against general-purpose servers, development servers, managed platforms, and its closest small peers — and why simplicity and security usually trade against each other.
How it Works
The developer's view: the design principles, the architecture map, what each region of the module does, and how to work on Servette itself.
How to Use it
Getting a site live: installing the package, running the wizard, leaving it running, publishing from the browser admin page over your own SSH tunnel, and the full command reference.
The trade-off
Simple, or secure. Usually not both.
Serving a static site safely on the public internet is a solved problem twice over — and both solutions cost you something. The simple tools skip the security. The secure tools make you configure it. Servette's whole reason to exist is that this trade is not actually necessary at this size.
Simple, but not secure
http.server, miniserve, and most one-command file servers. Running in seconds, and with no trusted TLS, no security headers, no rate limiting, and nothing that survives you closing the terminal.
Secure, but not simple
nginx, Apache, Caddy. Everything you need is available — after a configuration language, a certificate tool to wire in, and a large system you take on trust.
Servette
Secure defaults with nothing to configure, because the job is narrow enough that every secure choice can simply be made for you.
The cost is real and worth naming: it will never do more than serve files.
By category
The three ways people serve a static site
Most tooling sits at one of three extremes. Each is a reasonable answer to a different question.
| Category | What it gives you | What it costs |
|---|---|---|
| General-purpose | nginx, Apache, Caddy. Any site, at any scale, with every feature you could need. | A configuration language to learn, a certificate tool to wire in, and a large system you take on trust rather than read. |
| Development | http.server and friends. A folder served in one command, right now. |
By its own documentation, not built to face the internet: no trusted TLS, no auth, no hardening, gone when the terminal closes. |
| Managed | GitHub Pages, Netlify, Vercel. They run it for you, and run it well. | Their infrastructure, their terms, their limits. You do not own the thing serving your site. |
| Servette | A static site you own, served securely from your own machine, set up by a wizard. | It will never do more than serve files. That is the deal. |
Peer comparison
The two extremes, in one table
The common ways to serve a folder sit at two extremes: the general-purpose servers can be made secure, but are not simple; nanoservers are simple, but not secure.
| Servette | nginx | Apache | miniserve | Static Web Server | |
|---|---|---|---|---|---|
| Serves a folder in a single command | ✓ | ✗ | ✗ | ✓ | ✓ |
| No config language to learn | ✓ | ✗ | ✗ | ✓ | ✓ |
| Trusted HTTPS with auto-renew | ✓ | Configurable | Configurable | ✗ | ✗ |
| Security headers and rate limiting | Automatic | Configurable | Configurable | ✗ | ✗ |
| Survives a reboot as a service | ✓ | ✓ | ✓ | ✗ | Configurable |
| Readable source | literate Markdown | large C binary | large C binary | Rust crate | Rust crate |
Both extremes are right for their jobs: nanoservers are appropriate for building and testing, while production servers such as nginx and Apache are appropriate for more complex deployments, including large server-side applications. Servette is for the gap between: production security with nanoserver simplicity, from a single module you can read. (Peer columns hand-checked 2026-08.)
Secure by default
What you get without asking
Every item below is on from the first run. None of it is an opt-in, a plugin, or a setting you have to discover.
| Default | What it does |
|---|---|
| Trusted HTTPS | A Let's Encrypt certificate, issued at setup and renewed automatically; plain HTTP is 301'd upward. |
| Security headers | HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy, and Permissions-Policy on every response. |
| Rate limiting | Two independent sliding windows per IP — total requests, and failed logins — making password guessing impractical. |
| Path traversal refused | Every request path is resolved and checked against the served directory; anything outside it gets a 403. |
| Connection cap | Concurrent connections are capped and closed rather than queued, with a per-connection timeout — a slowloris mitigation. |
| Least-privilege service | Runs as its own login-less user under a systemd sandbox: no new privileges, a read-only system, and write access to its own directory only. |
| Nothing writes on request | Serving a request is read-and-send. No request from the internet can cause Servette to write to disk — publishing runs on a loopback page reachable only over the operator's own SSH tunnel. See How it Works. |
| Optional password | Basic Auth per site when you want it, stored as a memory-hard scrypt hash, never in plaintext. |
| An error page that diagnoses | A miss returns 404 with a page that reports the live connection and whether your site root is published at all — not ten bytes of Not found. Your own 404.html replaces it. |
Simplicity, measured
The whole server, counted
The claim is not that Servette is a quick read — it is that one person can fully understand the whole server, in a weekend of honest work. This is the number that makes that possible: Servette is 6,816 lines of Python, of which 4,939 are code and the rest comments and blanks. It is one module you can read top to bottom — generated from literate Markdown sources you can read just as easily, in this site's own source viewer. The three embedded pages — the error page, the connection test, and the admin page — are authored HTML inlined at build time, counted apart from the Python here.
| Section | Responsibility | Total | Code | Share | |
|---|---|---|---|---|---|
| Init | Module preamble and imports | 82 | 52 | 1% | |
| Server | Everything on the request path | 1,706 | 1,095 | 25% | |
| System | Lifecycle, certificates, systemd | 2,008 | 1,529 | 29% | |
| Shell | The operator's side: admin page, publishing, settings, shell | 2,933 | 2,218 | 43% | |
| Main | The entry dispatch | 87 | 45 | 1% | |
| Total | 6,816 | 4,939 | 100% |
The shape is worth a second look: the part that faces the internet is the smallest of the three big sections. Well over half the module is setup, operation, and the operator's tools — work that happens before or between requests, where a mistake cannot be reached by a visitor.
Design philosophy
Everything necessary to do one thing well.
Servette's identity is a small set of non-negotiable principles — invariants, not preferences. Every design decision serves one of them, and a change that serves none is out of scope by definition. They are the lens for the only question that matters: should this exist in Servette?
| Principle | What it commits us to |
|---|---|
| Understood by one person | All of Servette is one literate module, small enough that one person can fully understand it — a weekend's honest work, not an afternoon's skim. No module sprawl, no hidden machinery. |
| Secure by default | Trusted TLS, HTTPS-only, security headers on every response, optional auth, rate limiting, a least-privilege service user. Security is the default state, never an opt-in. |
| Production-grade | Makes the stdlib's http.server fit to serve real sites on the public internet: automatic renewal, auto-restart, survives reboots. The production layer, not a dev tool. |
| Zero-friction | Install the package, run it, follow the wizard. No configuration language, no manual certificate or dependency management. |
| Minimal footprint | The standard library plus a single package, cryptography; transport, TLS, and the ACME client are stdlib or hand-rolled. Light enough for a Raspberry Pi. |
Minimalism is the default; those principles are the only licence to add complexity. The decision rule for any proposed change: complexity is earned only by serving a non-negotiable principle. Complexity justified solely by capability — other servers do it — is rejected.
When principles pull against each other, the principle wins over raw line count. HSTS, CSP, the ACME client, and the rate limiter all cost complexity and all earn it under "secure by default." That is the only permitted compromise to minimalism: another principle, never mere feature completeness.
The invariant
At request time, Servette never writes to disk and never evaluates user input.
One property underwrites the whole security story, and it is worth naming on its own — because it, not the file types served, is what makes the guarantees hold.
Serving a request is read-and-send, nothing more. Several security properties are not features anyone coded; they are what you get for free by never doing certain things — a read-only served tree, a closed threat model, a systemd sandbox that never has to widen.
Every write Servette does perform — config, certificate issuance, the ACME challenge file, published site content — happens in the shell, on the loopback admin page the operator reaches over their own SSH tunnel, or in a background thread; never on the public serving path. A proposed change is measured against this invariant first: if it adds a request-time write, or evaluates request input, it breaks guarantees the static design gets for free. That is a different program, not a feature.
The map
Four regions, one boundary
One file, but not one undifferentiated mass. Servette divides into four regions, and the line between two of them is the invariant above, drawn in code: nothing a visitor can reach has a path to a write.
Read the diagram for the consequence: there is no arrow from anything a visitor touches into anything that writes. That is not an access-control rule that could be misconfigured — it is the shape of the code.
Region 1 of 4 · read-only
Server — everything on the request path
1,706 lines. The only region a network request can reach, and the only one whose correctness a stranger can probe.
Config. A Config object reads and writes servette.toml; every field has a default. reload_if_changed() runs on every incoming request, so edits take effect without a restart — an edit no door would accept is refused whole, fatal at startup and last-good on the live reload. Passwords are hashed with scrypt (memory-hard; N=2¹⁴, r=8, p=1) and never stored in plaintext; the file is owned by the service user with the operator's group at 0640, so the operator reads their own settings without a password prompt and no other account can.
Rate limiter. Two independent in-memory sliding-window dicts — total requests (default 120/min) and failed auth attempts (default 6/min) — under a lock, keyed per address, with a native IPv6 address bucketed by its /64 so one visitor's 2⁶⁴ addresses cannot each claim a fresh allowance. The auth limiter activates only when credentials are actually submitted. X-Forwarded-For is trusted only when a trusted_proxy is configured, and only its rightmost value: one hop. Stale-IP eviction runs in a background thread every 30 seconds, off the request hot path.
File cache. Files are read once and cached by path; compressible types are also gzip-stored, and the right encoding is sent per Accept-Encoding. A file too large to fit is served raw without being stored, so it cannot purge everything else. The file's identity — mtime, size, and inode — is re-checked on each request; this is the live reload. ETags (SHA-256 of contents) drive 304 responses.
Request handler. _handle_request is the transport-agnostic core: site selection → rate limiting → auth → path resolution → file serving. It takes a method, path, and headers and returns (status, headers, body) — a pure function the transport just feeds and sends, with no socket and no framework. Path resolution enforces traversal protection (403), refuses hidden paths — a stray .git or .env under the served folder is never served — falls directories back to index.html, serves a custom 404.html if present, and honours single byte ranges for media seeking. Per-site redirects, validated at load, answer before the filesystem is touched.
HTTP servers. A threading server terminates TLS — one ssl.SSLContext per active site, selected by SNI, so each domain is served its own certificate — and performs the handshake on the connection's own worker thread, not the accept loop, so one slow handshake cannot stall new connections. A bounded semaphore caps concurrent connections — past the cap they are closed rather than queued — and a per-connection timeout reaps slow or idle ones. Startup fails closed: the bind and the certificate load both happen synchronously, so a port conflict or unreadable cert raises immediately rather than leaving a process that looks healthy and serves nothing.
Region 2 of 4 · writes, off the request path
System — the environment around the server
2,008 lines. Everything that runs on its own schedule rather than on anyone's request.
Startup refresh. There is no bootstrap: pip owns the environment, and systemd invokes the installed package directly. What runs at startup instead is a drift gate — the systemd units Servette writes are stamped with the version that wrote them, so a shell launched from a newer install notices the stale stamp, refreshes the units, and restarts the service before doing anything else.
Server lifecycle. start_server() and stop_server() own the HTTP servers, their daemon threads, and the background workers. Under --serve the main thread then blocks watching the HTTPS thread's liveness, and exits nonzero once it has been dead past a grace period — so systemd's Restart=always brings the service back. Without that watch, a dead server thread would leave a living process systemd reports as healthy.
Production honesty. _production_issues() returns the conditions blocking production readiness — serve directory missing, certificate missing or self-signed, a username with no stored password, a swapfile absent or undersized for the host, low disk where content lands — printed on startup and on every status. It refuses to imply production-ready while anything is wrong.
Certificates. Let's Encrypt certificates come from Servette's own minimal ACME client: RFC 8555 HTTP-01 over stdlib urllib, with cryptography for JWS signing and the CSR. It is deliberately narrow — HTTP-01 only, no revocation or key rollover — which is why it fits in one file instead of pulling in the certbot stack. It first attempts a certificate covering both the domain and its www. form, falling back to the bare domain if only that fails DNS validation, and saying so.
Cert watchdog. A daemon thread polling every 60 seconds. The renewal check itself does not need that cadence — it reads an expiry date and acts only inside the last 30 days — but the same pass also detects a certificate rotated on disk by an outside tool, by mtime, and reloads the server for it. That is the case worth catching quickly, and an expiry check is cheap enough to ride along on the same tick. Renewal failures back off to at most one attempt an hour. Each pass contains its own exceptions: a failed pass logs and retries rather than killing the thread, since a dead watchdog would silently end renewals.
systemd and host provisioning. enable creates the login-less servette user and writes a sandboxed unit: CAP_NET_BIND_SERVICE to bind 80 and 443 without root, with NoNewPrivileges, ProtectSystem=strict, PrivateTmp, and write access limited to the server's own directory and the ACME webroot. Install also provisions two host-level defenses born of a production post-mortem: a network watchdog that restarts the host's network manager if the default route disappears, and a swapfile offer sized from measured RAM and demand rather than a hardcoded ceiling.
Updates. Servette updates the way every pip-installed tool does — pipx upgrade servette — and rolls back by installing the version you want. The next shell launch notices a stale service unit and refreshes it where it has the permission, or says so. Site content travels its own channel entirely: the operator publishes it, from the admin page over their own SSH tunnel or from the server's terminal — nothing is ever pushed to the box. Settings are never touched by either.
Region 3 of 4 · read+write
Shell — the operator's side
2,933 lines, the largest region: the loopback admin page, content publishing, the settings doors, and the terminal shell — reachable only by an operator over SSH.
The REPL shown when running without --serve, dispatching to setup, config, enable and disable, start and stop, status, sites, set, log, traffic, admin, publish, and restore-site. Every scalar setting has one door, set, which validates every pair before applying any; the config sub-shell holds the flows that need a guided prompt — the site list, certificate issuance, the login pair.
The admin page is the browser half of the same surface: admin serves one embedded page on 127.0.0.1 only, for exactly that command's lifetime, reached through the operator's own SSH tunnel with a one-time passcode per run. Three tabs — one card per site (publish, preview, certificate, access, redirects, kept versions), the server's own status and settings, and traffic statistics. Every button runs the same core the matching terminal command runs, so the two surfaces cannot disagree.
Publishing lands through one pipeline whichever surface starts it: the bundle is staged, its extraction guarded against traversal and decompression bombs, and a symlink flip swaps it live atomically — old or new content, never neither. The five most recent versions are kept, and restore-site (or the card's Restore button) is the same flip in reverse, consuming nothing.
That this is the biggest region is the point rather than an accident. The interface a human uses is where care is cheapest to spend and mistakes are least dangerous: nothing here is reachable from the public internet.
Region 4 of 4
Init and Main — the bookends
82 and 87 lines: the module preamble and imports at the top, and at the bottom the dispatch that decides which of the other three regions you get.
Run with --serve and it is the server; run without and it is the shell. Everything else follows from that fork.
Read the source
The source viewer
Servette's module is generated from five literate Markdown files, where the prose explaining a piece of code sits directly beside it. The source viewer is how you read them — in the browser, with no checkout.
The authored bytes, not a copy
Each file is fetched from the repository as the page renders, so what you read is what is in the repository — never a stale duplicate. A ?ref= parameter picks the branch or tag.
Built to navigate
Prose renders, Python is highlighted, and every code block is a named cell you can jump to. Alongside it: search across all five files, a rendered preview of the open file, and a call map of its functions.
The reading half of a real tool
It began as the notebook interface Servette was written in, with everything that edits removed. It stores nothing about you and runs entirely in your browser.
Working on it
Developing Servette itself
Two entry points, one test command, and a verification bar that stands between any change and main.
Running
servette # interactive shell — asks for root when it needs it servette status --json # one-shot command, for scripts and tools python3 -m servette --serve # non-interactive service mode (used by systemd)
Tests
# any Python with cryptography available, e.g.
python3 -m venv .venv && .venv/bin/pip install cryptography
.venv/bin/python3 tests/test.py
The suite starts a real server on a test port, runs its checks, and tears down; it backs up and restores any existing servette.toml. It requires openssl on PATH, used only to generate a throwaway certificate during setup. A browser leg drives the admin page in a real Chromium via Playwright; without Playwright installed those checks are skipped, with a printed hint.
Intentionally not covered end to end: live systemd operations and real Let's Encrypt issuance — each needs external infrastructure. Their seams are covered at the unit level instead: shell dispatch runs under scripted input, generated unit files are checked against systemd-analyze where available, and restore-site, the prompts, and the install helpers have direct tests.
The verification bar
Servette is a security tool, so a claim about it may never sit above its evidence. Three gates stand between a change and main, enforced by branch protection.
| Gate | What it means |
|---|---|
| Tests green | tests/test.py passes against the pip-installed package on the supported Python versions — CI runs 3.11 through 3.14 on Ubuntu, plus Debian 12. A behaviour change ships with the test that would have caught its absence. |
| CodeQL clean | No new alerts. Standing alerts are either fixed or dismissed with a recorded reason, so "clean" means clean, not "no new noise." |
| Human read | Any change touching auth, TLS, rate limiting, or path resolution is read by a person for what it claims, not only for what the tests assert. |
Prefer understatement. _production_issues() is the model: it lists what is wrong rather than implying everything is fine. The failure mode to guard against is never fabrication — it is a claim quietly stronger than its evidence.
Development happens on one short-lived branch per merge, via pull request; main is protected against direct and force pushes. __version__ never moves during ordinary development — it changes only when cutting a release, a maintainer task; publishing to PyPI rides GitHub's Trusted Publishing, with no long-lived credential to leak.
Getting started
How to use it
Servette needs Python 3.11 or newer and a server you can SSH into. It installs like any Python tool — one package, bringing its one dependency, cryptography, with it. You don't write a config file — the wizard asks for what it needs.
Install the package
One package from PyPI; pipx gives it an isolated environment and puts servette on your PATH.
# on your server sudo apt install pipx # if it isn't installed yet pipx install servette
Run the wizard
It asks for your domain, an email address for the certificate, and — if the site should be private — a password.
servette
servette> setup
Leave it running
Installed as a system service, it restarts itself.
servette> enable servette> status running · https://example.com
The wizard issues a Let's Encrypt certificate, redirects plain HTTP up to HTTPS, and installs a systemd service so the server comes back after a reboot. Renewal happens on its own: a background watchdog renews the certificate once it is inside thirty days of expiring, so it never lapses and you never touch it.
Setup writes nothing into your site folder, and it does not need to: a site with nothing published answers its own domain with Servette's error page, which reports that the server is up and what the connection is actually sending. That page rendering over HTTPS is itself the end-to-end proof — the server, the certificate, and the HTTP-to-HTTPS redirect all working. Publish your content whenever you are ready; nothing writes into a site folder except a publish you perform.
It is on this server too, and it answers in both of its roles: asked for directly it is the connection test and returns 200; arrived at by a miss it is the error page and returns 404. One page either way — the same checks, on the same server, over the same certificate. Only the status and the framing differ; what each role is for is further down, under Your 404 page does the diagnosis.
Serving over a self-signed certificate works too, for a local network or a machine without a public domain. The browser will warn once; everything else behaves the same.
Reference
The shell
Running servette without arguments drops you into an interactive shell; servette <command> runs one command and exits, which is how scripts drive it. Everything is a command: the settings file is Servette's to write, not yours to edit, and nothing needs a restart to take effect.
Main shell
- setup
- guided walkthrough for getting started
- config
- view and edit settings (the guided flows)
- start / stop
- start or stop the server
- enable / disable
- install or remove the system service
- status [--json]
- show whether the server is running
- log [n]
- show the last n log entries
- traffic
- requests, statuses, and top paths, from the log
- sites [--json]
- list configured sites
- set [n] k=v …
- change settings non-interactively
- admin
- the browser admin page, over your SSH tunnel
- publish [n] <folder>
- publish a folder on the server as a site's content
- restore-site [n]
- roll a site back to a kept version
- help / quit
- show commands, or exit
config sub-shell — the guided flows
- sites
- list configured sites
- add-site
- add a site — domain and password; Servette assigns the folder
- remove-site <n>
- delete a site's server copies with its config
- move-site <n> <to>
- reorder the site list
- cert [n]
- certificate issuance for a site
- username / password [n]
- the login pair, prompted interactively
- show / back
- print settings, or return to the main shell
set — one setting, without the prompts
- set k=v
- a server-wide setting: port, email, rate_limit, auth_rate_limit, cache_size_mb, trusted_proxy, health_path, tls_min_version, ciphers, csp, permissions_policy
- set n k=v
- one site's setting, by its number in
sites: username, active, cache, redirect —set 1 active=notakes site 1 offline
Settings live in /var/lib/servette/servette.toml, owned by the service user with your group at 0640 — everything Servette keeps lives under /var/lib/servette, and SERVETTE_HOME moves it. Every field has a default, and the file is re-read whenever it changes — so a setting takes effect on the next request, not the next restart. A value that isn't allowed is refused with a sentence naming the rule — the same answer from set, from the guided flows, and from the admin page.
Publishing
Updating your site
Content reaches a site exactly one way: you publish it. There is nothing to configure — no account, no signing key, no hosted shelf — because the transport is the authentication: every hop is your machine or your SSH connection. Nothing is ever pushed to the box.
1 · The admin page — from your own computer
The everyday route: the folder is on your own computer, and the browser does the rest.
One page, over your tunnel
Run servette admin over SSH and open the printed link. The page is served on the box's own localhost and rides your SSH connection — it exists nowhere on the public internet, and a one-time passcode per run is the login.
Drop, preview, publish
Drop your site's folder on its card. The bundle is built in your browser, hidden files excluded by the same rule the server serves by. Preview first if you like: the draft is served over the same tunnel, unpublished, with links and stylesheets resolving.
Atomic, with history
Content is staged, its extraction guarded, and swapped live in one atomic flip — old or new content, never neither. The five most recent versions are kept, and Restore puts any of them back as instantly as a publish.
2 · The terminal — when the folder is on the server
The same machinery from the shell, for content already on the box and for scripts.
One command
publish 0 ~/sites/blog tars the folder on the server and lands it through the identical pipeline — same caps, same extraction guards, same atomic swap into the same version ring. Scripts drive it one-shot over SSH: ssh box servette publish 0 <folder>.
Undo is a command too
restore-site lists the kept versions and flips the one you pick live. The tree being rolled away stays in the ring, so restoring — even the wrong version — is undoable in its own terms.
Servette's own code travels a different channel entirely — pip delivers it, and rolls it back (pipx install --force servette==x.y.z) — and neither channel ever touches your settings.
Either route keeps the trigger with the operator: both ride your SSH connection. No network-reachable trigger exists, and no request from the internet can cause Servette to write anything.
Verifying
Your 404 page does the diagnosis
Every server needs an error page. Most spend the whole response telling the reader they were wrong. Servette's default one also reports that the server is up, which host answered, the path that was asked for, what the response actually carries — and whether anything is published at your site root at all, which is what tells a mistyped link apart from a deploy that never landed. The status is still 404 — a missing path is a missing path, and nothing here bends a status code to look better than the thing it reports. Only the body changed. Write your own 404.html and it takes over; until then this is what a miss returns. The connection test itself lives at /.well-known/servette-check on your own site whenever you want to ask it directly.
Right after setup
You do not have to go looking for it: with nothing published yet, your own domain answers with it. The certificate chain, the HTTP-to-HTTPS redirect, and every security header, as seen from the public internet rather than from inside the box. status says the process is alive; this says the internet agrees.
After you change a setting
Changed a password, or one of the security settings? Reload the page and see whether the change reached your visitors. A setting that is saved and a setting that is being sent are not the same claim.
After a publish
The is-anything-published row separates a mistyped link from a deploy that never landed — the check runs on the site that served it, so it reports what visitors are actually getting, not what you meant to ship.
What it reports: the security headers, a 304 on revalidation, 405 on a POST, 404 on a missing file, 403 on a traversal attempt. Anything it cannot judge from a browser is marked skipped — never passed — and an optional header you switched off reads as a choice, not a defect.
It is same-origin by construction, which is exactly what lets it read what a cross-origin checker never could. The same property means it only ever reports on the server that served it — ours reports on ours, yours on yours. Your 404.html wins the default-miss body by existing; the check page's path is reserved (under /.well-known/, the namespace the web sets aside), so it keeps answering even then.
What it will not do is guess. It never lists your directories or suggests near-miss filenames, however helpful that would be to you, because the same suggestions handed to a stranger turn an error page into a file-discovery tool. And in the error role it drops the paragraph describing Servette's other features: your error page is not our advertising space.
Both buttons open the same page on this server, over the real certificate. What differs is how you arrive at it — which is the whole design, and the only way to see it is to arrive both ways.
Ask for it directly · 200 Arrive by a miss · 404
The second asks for a path that genuinely is not here, and that is the only way to see the two rows the first cannot: the path you asked for, echoed back, and whether anything is published at this root. The check path is reserved, so it is always present and always answers — a page that can never be a miss cannot demonstrate one. It is the same report either way; missing is what gives two of its rows anything to say.