Architecture
Stimpunks.World is a directory of static files that Netlify serves as they are, deployed by a push to main. The CB’s functions and one storage area are the only parts that run on a server. This page is the code and the hosting; How this site is made is the why. Last updated 2026-10-04.
At a glance
Ryan’s and Helen’s machinesHand-written pages, love.css and scripts. Data files become page regions through tools/make-*.py. tools/check-all.sh runs before every commit.
Ryan’s laptop, 06:10 each morningtools/daily-*.sh read YouTube’s feeds and our sibling sites’ RSS, redraw one room each, and commit only that room’s files.
git push
GitHub: Stimpunks/Stimpunks-Love, mainEvery commit here is published. There is no staging and no review.
Netlify deploys it in a minute or two
Netlify · stimpunks.world
Static filesEvery page, love.css and the scripts. _headers sets the security policy and caching; _redirects sends the old addresses here.
Functions at /cb/*Signing on, the channels, calls, boards, the shared games and the Slake. One of them sweeps every hour.
One Blobs store, cbTen messages a channel, gone at midnight Colorado time. No IP address and no log.
your browser gets pages; the radio, once you sign on, fetches /cb/*
Your browser
Every page and love.jsNothing from outside loads. The dial is set before first paint. Only localStorage is kept.
The CB radio, once signed oncb.js, in a shadow root. It asks only while it is open and the tab is in front.
A press on a play buttonlove-embed.js builds the one frame or player, checked against its list of allowed origins.
only after a press
Third partiesyoutube-nocookie.com, open.spotify.com, embed.music.apple.com and videopress.com for players; 8x8.vc for calls, the only thing given the camera and microphone; the band’s own site for The Small Hours’ audio.
Hosting and deploy
Netlify serves the repository root as it is, and a push to main is a deploy: live in a minute or two, with no staging step and no review. There is no build command, no bundler and no fingerprinting. The HTML in the repository is what you are reading.
- Domain.
stimpunks.worldsince 2026-09-23.stimpunks.love(2026-09-19 to 2026-09-23) andstimpunks-love.netlify.appstay registered and send you here, path for path, with a 301. - Functions. Netlify bundles
netlify/functions/*.mjson deploy and installs the one dependency inpackage.json,@netlify/blobs. Nothing else is installed or built. - Storage. One Netlify Blobs store named
cb, opened with strong consistency. It is the only thing the site keeps on a server. - Rate limits. Each function declares Netlify’s own limiter in its config. The limiter counts addresses itself, so no IP address ever reaches our code or our storage.
| File | What it decides | Edited by |
|---|---|---|
netlify.toml | Publish the root, run no build command, skip asset processing | hand |
_redirects | The old domains and the netlify.app host to stimpunks.world; /netlify/*, /node_modules/* and package*.json answer 404 | hand |
_headers | Caching per path, the security headers, the Link header, X-Clacks-Overhead | hand, except the policy line |
_headers, the Content-Security-Policy line | frame-src, media-src and the hash of the snippet every page runs before first paint | tools/make-csp.py only |
package.json | @netlify/blobs, for the functions | hand |
.claude/launch.json | The local preview, npx serve . -l 8919 | hand |
Caching. The HTML, love.css, love.js and love-embed.js are checked again on every request (max-age=0, must-revalidate), because nothing is fingerprinted and a stale stylesheet against new markup is an unstyled room. Typefaces, audio/, raven/ and oracle/ are cached for a year; share cards and icons for a week; feed.xml, sitemap.xml and robots.txt for ten minutes.
Security headers. nosniff, Referrer-Policy: strict-origin-when-cross-origin, HSTS for two years, and X-Frame-Options: SAMEORIGIN with frame-ancestors 'self', because the laptop in the Solarpunk Hermitage frames this site inside itself. connect-src is 'self'. Permissions-Policy gives the players’ features to youtube-nocookie.com, the camera, microphone and screen to 8x8.vc only, and denies everything else.
Secrets live in Netlify’s environment, never in the repository. By name only:
| Variable | Used for |
|---|---|
CB_PASSWORD | The community password. Every pass is an HMAC keyed by it, so changing it and redeploying signs everybody off. |
CB_MOD_PASSWORD | The base station’s password, for moderators |
CB_MODS | Which handles hold which roles. Missing or unreadable, it fails closed. |
CB_JAAS_KID, CB_JAAS_KEY | The 8x8 key id and private key that sign call tokens |
CB_JAAS_EVENTS_SECRET | Checks 8x8’s webhook saying who is in a call |
CB_JAAS_HOOK_SECRET | An optional check on 8x8’s settings webhook |
The local server is not the site. npx serve sends no headers, so a policy failure only shows on the live host. It drops a query string on its clean-URL redirect, which Netlify does not. It runs no functions, so /cb/* answers 404 there; the CB runs under netlify dev after npm install, or live.
The front end
Every page is a hand-written HTML file at the repository root, complete before any script arrives, styled by one shared stylesheet and plain scripts with no framework and no dependencies. There is deliberately no design system: each room is its own visual world, and the code is arranged so that stays true.
A page. Its <body> class names the room, and that class is the hook for the room’s section of love.css and for its share card. The <head> carries, in order:
- the pre-paint snippet, the same on every page byte for byte, which sets the dial from
localStorageorprefers-reduced-motionbefore first paint; its sha256 is in the security policy; - the canonical address, the description and the Open Graph tags, with the share card written by
make-og.py; - structured data from
tools/structured.py, built from the page’s own title, description and address; love.js, plus whatever scripts that room needs, all deferred.
love.css is one file in numbered sections. §1 is the self-hosted typefaces, with fonts/_sources.json recording where each came from. §2 is ground rules: every colour as a custom property on :root, and a global rule that keeps a hidden thing hidden. §3 is the intensity dial. §4 is shared furniture that is layout and never colour: the back link, the sign-off, the press-to-play plate, the job marker. Then each room, area or piece of street furniture gets a self-contained section, duplicating ideas in its own clothes on purpose. Small screens and print close the file. check-classes.py refuses a class claimed by two rooms, a custom property declared twice, and a section number out of order.
The dial. Gentle, Regular and MAX change motion and sparkle only, never content. §3’s reset carries !important, and rooms scope their own tilts with :where() so the reset can win. check-gentle.py measures every page at all three settings.
| Script | Loaded | Job |
|---|---|---|
love.js | every page | The dial, the Playhouse toys and the superposition panel, and two loaders: the radio and the fractal window |
love-embed.js | pages with something to press | The only code that builds a frame or an audio player. It holds the lists of origins it will frame and play from, and the call’s. |
rack.js | rooms with a rack | A rack’s screen and each card’s second button, found by data-rack-* attributes and never by class |
quest.js | every page with a job marker | The Adventurer’s Guild’s markers. The markers themselves are <details> and work without it. |
finder.js | the front page and the map only | Search over room names, descriptions and search-index.json |
cb.js and cb.css | only once signed on to the CB | The radio, in a shadow root so no room’s styles reach it |
guest.js | not signed on | The radio’s bar folded shut: the teleporter and the way on. It knows nothing about the channel. |
call.js, callroom.js | on a press | A room’s 8x8 call window, and the mover the radio shares |
fractal.js | on a press | The sign-off’s fractal window, limited so it cannot flash |
| a room’s own script | that room’s page only | Its game, toys or machine: arcade.js, mud.js, stay-breezy.js and the rest |
Press to play. A play button is a <button class="facade"> that names the thing and how long it runs. Nothing reaches a third party until it is pressed. Then love-embed.js checks the address against its list and swaps the button for a youtube-nocookie.com frame, or another origin on the list. A room styles the pressed state on the container, because the button’s classes do not survive the swap.
What your browser keeps. Only localStorage, and only in your browser: love-intensity (the dial), love-cb (your CB pass and handle), love-cb-guest (the guest panel left open), love-quests (the Guild’s list), love-rescues (animals you rescued) and love-breezy (Stay Breezy’s usual fans). Privacy says the same. Every read and write is wrapped so a private window still works.
Machine-readable files, all generated: sitemap.xml, llms.txt, feed.xml (from the changelog), cb-rooms.json, search-index.json, /.well-known/security.txt, /.well-known/api-catalog and the agent skill under /.well-known/agent-skills/.
Data and generators
The pages are hand-written, but any part of a page that restates a list, a credit or a fact held somewhere else is generated from a JSON file by a Python script, so the two cannot drift. The scripts are local tools in tools/; none of them runs on a server. The usual path is data/<room>.json to tools/make-<room>.py to the room’s page, between markers, and usually to the liner notes as credits too.
Marker regions. A generator only ever rewrites text between a pair of comments such as <!-- jukebox:begin --> and <!-- jukebox:end -->. Everything outside the markers is hand-written. Every page has at least the sign-off and the structured data between markers. A room somebody built with their own AI and brought in through Your Room sits between import:<slug> markers.
Generators refuse rather than guess. Each one checks its data against the promises its room makes before writing: a runtime on every track, a credit on every quotation, a consent record on every photograph, an allowed origin on every player. A refusal stops the build and nothing after it runs.
| Kind | Tools | What they write |
|---|---|---|
| Room builders | make-<room>.py, one per room with data | That room’s marker regions, and usually its credits in the liner notes |
| Readers of the rooms | make-now-playing.py, make-map.py, make-signoff.py, make-structured.py | Now Playing, the map, every sign-off, every page’s structured data. They read the published pages, so they run after the builders. |
| Site files | make-sitemap.py, make-search-index.py, make-feed.py, make-agent-files.py, make-security.py | sitemap.xml, llms.txt, cb-rooms.json, search-index.json, feed.xml, /.well-known/* |
| Headers | make-csp.py | The policy line in _headers, from love-embed.js’s origin lists and the pre-paint snippet’s hash |
| Images | make-webp.py, make-og.py, make-icons.py | WebP only where it is as faithful (SSIM 0.98 or better) and at least 10% smaller; a share card per page in og/; the favicons |
| Network pulls | pull-arrivals.py, pull-doom-scoop.py, pull-vital-rack.py, pull-notebook-month.py, pull-large-month.py, pull-cookin-month.py, pull-club.py, pull-foundry.py | A data file refreshed from feeds or GitHub. Never run by the build. |
| Intake | intake-collection.py, import-room.py | Metadata stripped from arriving photographs; a room somebody built with their own AI, brought in |
A generated file or region is never edited by hand. That covers feed.xml, sitemap.xml, llms.txt, cb-rooms.json, search-index.json, everything in og/, data/og-prints.json, the policy line and anything between markers. On a merge conflict in one of them, take either side, fix the source and run the build again.
Share cards. make-og.py drives headless Chrome to photograph a scrap of markup on the page’s own body class with love.css attached, and lifts the page’s heading word for word. There is a card design per room and no template. It keeps the committed picture when nothing that decides it has changed, because two Macs draw the soft edges of letters differently.
Checks
tools/check-all.sh is the build and the test suite in one. It runs every generator and every checker in the one order that works, stops at the first refusal, and changes nothing on a clean tree. It runs before every commit; Netlify does not run it.
- The Node tests.
netlify/cb/lib.test.mjsraces writers against each other and fails if any write that was told it worked is missing.tools/fractal.test.mjscounts flashes in what the fractal window paints. - Images, before anything refers to one.
- The room builders, each from its own data file.
- The things that read the rooms: Now Playing, the map, sign-offs, structured data, the sitemap, the search index, the feed, the policy line, the agent files, share cards, icons and
security.txt. - The checkers, against what was just written.
| Checker | Refuses | Renders in Chrome |
|---|---|---|
check-contrast.py | A text and ground pair under WCAG 1.4.3, and any colour on :root that is neither measured nor exempted with a reason | no |
check-contrast-live.py | Rendered text against the ground it actually sits on. It declines rather than guesses on a gradient. | yes |
check-focus.py | A focus ring that cannot be seen against its ground | yes |
check-gentle.py | Rotation, skew or ambient motion left at Gentle, and content that changes between dial settings | yes |
check-print.py | A zine page that is not one sheet, a broadside that is not two pages a sheet, an ink not on the allowed list | yes, to PDF |
check-weights.py | Two of the Foundry’s weights that render the same letters | yes |
check-headings.py | A skipped heading level, or other than one <h1> | no |
check-ids.py | An id used twice on a page, or a script asking for an id no page has | no |
check-classes.py | A class claimed by two rooms, a custom property declared twice, a section number out of order | no |
check-counts.py | A sentence stating how many rooms, pages or typefaces the street has | no |
check-quests.py | A job code that disagrees between a room and the Guild’s board | no |
check-faces.py | A typeface in fonts/ missing from the Foundry or the Mopery’s picker | no |
check-teleport.py | The room search drifting apart between the radio, the guest bar and the finder | no |
Left out of the build on purpose: anything that needs the network. check-jukebox.py asks YouTube’s watch page whether every play button’s video still plays and still embeds, and it is run by hand. A gate that fails on a train is a gate people learn to skip.
When a tool refuses, the cause gets fixed. Loosening a check, or adding an exemption to quiet it, is the one change the repository’s rules forbid outright.
Server side: the CB
The CB, a chat channel patterned after citizens band radio, is the only server-side code on the site: Netlify Functions in netlify/functions/ sharing one library, netlify/cb/lib.mjs, and one Netlify Blobs store. Every promise on Privacy is kept in that library.
How it runs. Each function is a JavaScript module whose config gives its address and a Netlify rate limit. A request carries the pass from the browser, readPass() checks it, and a change is written only if the stored copy has not changed since it was read, so people writing at once never lose a message. One scheduled function, cb-midnight, runs every hour and sweeps whatever has expired. It runs hourly because midnight in Colorado moves between 06:00 and 07:00 UTC.
The radio only asks while it is open and in front. One function in cb.js decides; folded, or in a background tab, the radio sends nothing. Open, it listens every four seconds. Tuned to World it says nothing about where it is; tuned to a room it names that room.
| Area | Addresses | What is kept, and for how long |
|---|---|---|
| Signing on | /cb/signon, /cb/account, /cb/account/recover, /cb/account/admin | A pass is an HMAC keyed by the password, kept only in your browser. A claimed username is a salted scrypt hash, a version and a lock, filed under a hash of the name. No email, and no list of accounts. |
| The channels | /cb/channel, /cb/transmit, /cb/moderate, /cb/react, /cb/image | Ten messages per channel (World, or one per room), gone at midnight Colorado time. Pictures are redrawn in your browser, refused if they still carry metadata, and deleted with their message. |
| Being seen, and watching together | /cb/here, /cb/here/leave, /cb/beacon | One record per visit, shown for 30 seconds after its last check. A host’s beacon is replaced, never added to. |
| Calls | /cb/call, /cb/call-events, /cb/call-settings | Nothing about a call. It signs an 8x8 token for one handle and one room, and reads 8x8’s webhooks for who is in a call. |
| Boards | /cb/chalk*, /cb/pebbles*, /cb/brass*, /brass-tacks.xml | The chalkboard and the pebble bowls: a week. The Brass Tacks Board: until a moderator takes a post down. |
| Shared games | /cb/pando*, /cb/fridge*, /cb/stair*, /cb/shelter*, /cb/pets*, /cb/adopted | A tree’s count, finished sentences and a stair’s step, kept with no name in them. Adopted pets, under a hash of the handle, until Forget Me. |
| The Slake | /cb/mud/here, /cb/mud/say, /cb/mud/leave, /cb/mud/moderate | Presence per visit, replaced on moving. Each place’s talk is gone at midnight. |
| The sweep | cb-midnight, scheduled | Deletes what has expired across all of the above |
Never stored: an IP address (Netlify’s limiter counts them and never hands them over), anything a person types in a log line, an email address, or a list of who is on.
Roles. CB_MODS says which handles hold which roles, and it is read on every request. The server decides who may enter each private room in the Town Hall, in one function. A page that hides its radio from the wrong person is only keeping quiet; it is not the lock.
Calls are 8x8’s Jitsi as a Service. The CB signs a token with CB_JAAS_KEY for one room, and nobody joins without one. Guests get a token, never a moderator’s, only for the rooms Cavendish Coworking keeps open to the world. love-embed.js builds the call’s frame, and 8x8 is the only origin given the camera, microphone and screen.
Scheduled jobs
The rooms that show other sites’ feeds are snapshots refreshed by Ryan’s laptop each morning, not live pages. None of those hosts lets a page read its feed from another site, and the alternatives were a proxy we do not run or a third-party reader. Each room prints when it was last set, so a missed morning leaves an honest page rather than a wrong one.
The task. A Claude Code scheduled task, arrivals-board-daily, runs at 06:10 local time on Ryan’s machine. Its instructions are tracked in .claude/scheduled-tasks/arrivals-board-daily/SKILL.md, and tools/install-scheduled-task.sh installs that copy and reports drift. The schedule itself lives in the scheduler, not the repository.
Each script does the same five things. It refuses unless the tree is on main and its own files are clean. It pulls, which is the only network step; redraws its room; runs the fast offline checkers as a gate; then commits only its own files under a fixed subject and pushes. A failed push is left for a person. The scripts never rebase or force.
| Script | Room | Commits | Subject |
|---|---|---|---|
daily-arrivals.sh | The Feed | data/arrivals.json, the-feed.html | daily arrivals board reset |
daily-scoop.sh | The Doom Scoop | data/doom-scoop.json, the-doom-scoop.html, now-playing.html | daily doom scoop edition |
daily-vital.sh | Vital Plant Living’s telly | data/vital-rack.json, vital-plant-living.html | daily vital telly refill |
daily-notebook.sh | The Open Notebook | data/open-notebook-month.json, the-open-notebook.html | daily open notebook refill |
daily-large.sh | Learning Large | data/learning-large-month.json, learning-large.html, now-playing.html | daily learning large refill |
daily-cookin.sh | Hey, Good Cookin’ | data/hey-good-cookin-month.json, hey-good-cookin.html, now-playing.html | daily good cookin refill |
They run in that order, each whatever the last one returned, so one failing feed leaves the others filled. Every push deploys, so the morning makes a run of deploys. The commit subjects matter: the tool that writes Ryan’s weekly changelog skips exactly these.
Consequences. Before editing any of those files in the morning, pull first. If the laptop is asleep, the boards keep yesterday’s date. The build never pulls anything; the network tools run by hand are check-jukebox.py, pull-club.py and pull-foundry.py. The only job on Netlify’s side is the CB’s hourly sweep.
Third parties
A page load contacts nobody but stimpunks.world. The security policy makes that a property of the site rather than a habit: default-src, script-src, font-src, img-src and connect-src are all 'self', so there is no analytics, no CDN and no third-party script to remove. Every outside origin below is reached only after somebody presses something.
| Origin | Used for | Allowed by | Contacted when |
|---|---|---|---|
| www.youtube-nocookie.com | Videos and playlists in every room with a play button, rack or set | frame-src; the player’s features in Permissions-Policy | after a press |
| open.spotify.com | Club Chronic’s stage | frame-src | after a press |
| embed.music.apple.com | Club Chronic’s Apple Music deck | frame-src | after a press |
| videopress.com | Ryan’s own grass video in Swaying Sweetgrass | frame-src | after a press |
| 8x8.vc | A room’s voice and video call | frame-src; camera, microphone, screen and autoplay in Permissions-Policy | after a press, with a token the CB signed |
| josephmooon.wordpress.com, its uploads folder only | The Small Hours’ jukebox, streamed from the band’s own site | media-src | after a press |
Adding an origin is one edit. It goes in a list in love-embed.js, then make-csp.py runs. The policy line in _headers is never edited by hand: the next run puts it back, and the local server sends no headers, so the mistake only shows on the live site.
Playing is not the same permission as embedding. A service can refuse to be framed (stoat.chat, music.apple.com) or publish no player at all (Qobuz), and a video can have embedding switched off or be age-gated. Those become links shaped like doors, never frames, and check-jukebox.py asks YouTube for every play button whether it may still be embedded.
Other outside traffic, none of it from your browser:
- Netlify hosts everything, runs the functions and holds the store.
- 8x8 calls
/cb/call-eventsand/cb/call-settingsas webhooks, and both are signature-checked. - Ryan’s laptop reads YouTube’s channel feeds and watch pages and our sibling sites’ RSS each morning, and
pull-foundry.pyreads Google Fonts’ public repository when it is run.
Working on it
Ryan Boren and Helen Edgar both commit straight to main, one at a time, each working through their own Claude, and they agree outside the repository whose turn it is. A push is a deploy and nothing reviews it, so the routine is the safeguard:
- Pull before touching anything; the morning task will have pushed.
- Run
tools/check-all.shbefore every commit. Files it changes are generated files catching up with a source, and they go in with the change that caused them. - Commit and push when you stop. Uncommitted work blocks the other person and the morning task, which refuses to run over anybody’s edits.
- Never hand-merge a generated file. Take either side, fix the source and run the build again.
- A new changelog entry goes at the top with its own id, because the id is the feed item’s permanent address.
| What a machine needs | For |
|---|---|
| Python 3 | Every generator and checker |
| Google Chrome | The rendering checks, the print check, icons and share cards, all headless |
| Node | The local preview, the Node tests, and netlify dev for the CB |
cwebp, ffmpeg | A new JPEG arriving |
ffprobe | A new recording, because runtimes are measured off the file |
| Pillow | intake-collection.py |
| The Knowledge System mirror | Optional, and on Ryan’s machine only. Tools that read it say so and carry on without it. |
Where decisions are written. CLAUDE.md holds the rules sessions get wrong and why. DECISIONS.md lists what is open and what is settled. The changelog is the public record and feeds feed.xml. Privacy is the CB’s promise to the people on it. The liner notes hold every credit. SECURITY.md and /.well-known/security.txt say how to report a vulnerability privately.
Licence. Our words, markup and design are CC BY-SA 4.0. Photographs, recorded voices, songs and borrowed names keep their own terms, typefaces keep their own OFL or Apache 2.0 licences, and nothing musical is hosted here. The licence file says which is which.
Sources
Read from the repository on 2026-10-04: README.md, netlify.toml, _headers, _redirects, package.json, love.js, love-embed.js, the section headings of love.css, netlify/cb/lib.mjs and the config of every file in netlify/functions, tools/check-all.sh and the head of every checker, tools/daily-arrivals.sh, and the morning task’s instructions. When any of those changes in a way this page describes, this page changes with it, and the changelog says so.
HOW LOUD DO YOU WANT IT?
Starts wherever your device says. Nothing moves, flashes, or plays until you say so.