Before any soul is asked about
The first three lessons decided which soul may see which picture. This one decides which device may talk to the studio at all. One middleware function in app.py runs on every request before any route, and it applies its checks in a fixed order: the network, then the path's own rule, then the admin question. A request that fails one never reaches the next.
Gate 1: the network
require_family_network reads the TCP peer address from the socket, never from a header. It allows three things: loopback, Tailscale's IPv4 range 100.64.0.0/10 (the shared address space RFC 6598 reserves for carrier-grade NAT) and Tailscale's IPv6 prefix fd7a:115c:a1e0::/48. Anything else, a home LAN address included, gets a 403 with Use the family Tailscale connection.
The backend test makes the point with two clients. A peer on the home LAN sends x-forwarded-for: 127.0.0.1 and still gets 403, because no header decides this gate. A tailnet peer, say 100.64.0.7, gets a 200 from /api/health and a 403 from /internal/providers, because the internal paths have a rule of their own.
Headers matter once, in client_ip. It trusts x-forwarded-for only when the TCP peer is loopback, which is how Tailscale Serve's local proxy arrives when it carries the studio's HTTPS twin. Serve answers only inside the tailnet, so a browser that came through the twin passes gate 1 as loopback, and its real address travels on in the header for the authority to judge.
The internal paths are for engines on this Mac
Under /internal/ the middleware calls require_internal: the peer must be loopback and the request must carry none of x-forwarded-for, origin or sec-fetch-mode. Serve adds the first, so nothing that came through the HTTPS twin passes. A browser on the Mac itself is caught by the third: current browsers send sec-fetch-mode on every request to a loopback address. origin is the weaker check, because a browser leaves it off a same-origin GET or HEAD and off a no-cors GET such as an image load, so it catches cross-origin calls and POSTs, not every request. The gate leans on Fetch Metadata here: an old browser that predates it, opening an internal URL directly on the Mac, would carry none of the three. Apart from that corner, what is left is a plain HTTP client running on the same machine, such as Pippa, or the site's export gate through a tunnel that ends at loopback. These are the doors that take an actor_soul_id, and nobody outside the Mac can knock on them.
Gate 2: the same origin
Every /api/ path then passes same_origin. It refuses a request whose sec-fetch-site is cross-site, and one whose Origin names a host other than the Host header. Another page open in Dad's browser cannot drive the studio with his session. The session cookie is also SameSite=strict, so a cross-site request would not carry it anyway. Two independent locks on one door.
Gate 3: is this Dad?
Every /api/ path except four needs the admin check. The exempt four are /api/health, /api/config, /api/auth and /api/auth/status, the four a device must reach before it has unlocked: a health probe, the app's config, the PIN form and the lock status. require_admin asks two authorities, and Avatars keeps no users, PINs or sessions of its own.
- Firelink's family facade reports the device's security status. A blocked device or deep lockdown is a 403. When PINs are on and this device is not authenticated, the answer is a 401 with
Unlock with the family PIN. - Pippa's
/api/usersreports the active user. Without theis_admincapability, the answer is a 403 withAvatar administration is available to Dad.
Both calls carry the visiting device's own headers: its resolved address as x-forwarded-for, plus its origin, fetch site, cookie and Pippa session header. The module's docstring says it in one line: Avatars asks the family authority about the actual visiting device. Only after all of this does a request reach the studio routes that read pictures with no actor, the admin view of every soul from the first lesson.
The PIN and the cookie
POST /api/auth hands the PIN to the family authority through the kit's verify_pin_verdict, naming the device's address as the client. The authority binds the session it mints to that address. Without it, the engine's own loopback would answer for every device, and five wrong PINs typed anywhere would put loopback on the blacklist, after which every PIN proxied through an engine is refused. A verifier that cannot be reached is a 503, never a wrong PIN. On success the studio sets pippa_session as HttpOnly and SameSite=strict, and Secure when the request is HTTPS or came through the loopback proxy with x-forwarded-proto: https. Solo Auth Quest builds that PIN layer and Tailscale's role as the first line; this lesson only shows a sibling consuming them.
Shared connections, private credentials
Avatars keeps one pooled HTTP client for every upstream call, sixteen connections at most. Pooling is safe only because each call is built from scratch with exactly the headers it is given, and its comment says so: connections are shared, each visiting device's credentials are not. A test proves it. The upstream answers with a Set-Cookie, and the next request, sent without a cookie, carries none. A client-level cookie jar here would quietly let one device's session ride on another device's request.
Every response that reached a route then gets X-Content-Type-Options: nosniff, and everything under /api/ and /internal/ also gets Cache-Control: private, no-store, so no shared cache keeps a copy of a protected answer. The gates' own refusals skip both. When a gate raises, the except branch returns a fresh JSONResponse that never passes the two lines that add the headers. Those answers carry an error message and no picture, so nothing protected leaks, but the headers belong to the answers that reached a route, not to every response.