← BACK TO THE STREET

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

How a change gets from a laptop to a reader, and what talks to what.

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.

GitHub: Stimpunks/Stimpunks-Love, mainEvery commit here is published. There is no staging and no review.

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

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.

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.

FileWhat it decidesEdited by
netlify.tomlPublish the root, run no build command, skip asset processinghand
_redirectsThe old domains and the netlify.app host to stimpunks.world; /netlify/*, /node_modules/* and package*.json answer 404hand
_headersCaching per path, the security headers, the Link header, X-Clacks-Overheadhand, except the policy line
_headers, the Content-Security-Policy lineframe-src, media-src and the hash of the snippet every page runs before first painttools/make-csp.py only
package.json@netlify/blobs, for the functionshand
.claude/launch.jsonThe local preview, npx serve . -l 8919hand

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:

VariableUsed for
CB_PASSWORDThe community password. Every pass is an HMAC keyed by it, so changing it and redeploying signs everybody off.
CB_MOD_PASSWORDThe base station’s password, for moderators
CB_MODSWhich handles hold which roles. Missing or unreadable, it fails closed.
CB_JAAS_KID, CB_JAAS_KEYThe 8x8 key id and private key that sign call tokens
CB_JAAS_EVENTS_SECRETChecks 8x8’s webhook saying who is in a call
CB_JAAS_HOOK_SECRETAn 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:

  1. the pre-paint snippet, the same on every page byte for byte, which sets the dial from localStorage or prefers-reduced-motion before first paint; its sha256 is in the security policy;
  2. the canonical address, the description and the Open Graph tags, with the share card written by make-og.py;
  3. structured data from tools/structured.py, built from the page’s own title, description and address;
  4. 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.

ScriptLoadedJob
love.jsevery pageThe dial, the Playhouse toys and the superposition panel, and two loaders: the radio and the fractal window
love-embed.jspages with something to pressThe 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.jsrooms with a rackA rack’s screen and each card’s second button, found by data-rack-* attributes and never by class
quest.jsevery page with a job markerThe Adventurer’s Guild’s markers. The markers themselves are <details> and work without it.
finder.jsthe front page and the map onlySearch over room names, descriptions and search-index.json
cb.js and cb.cssonly once signed on to the CBThe radio, in a shadow root so no room’s styles reach it
guest.jsnot signed onThe radio’s bar folded shut: the teleporter and the way on. It knows nothing about the channel.
call.js, callroom.json a pressA room’s 8x8 call window, and the mover the radio shares
fractal.json a pressThe sign-off’s fractal window, limited so it cannot flash
a room’s own scriptthat room’s page onlyIts 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.

KindToolsWhat they write
Room buildersmake-<room>.py, one per room with dataThat room’s marker regions, and usually its credits in the liner notes
Readers of the roomsmake-now-playing.py, make-map.py, make-signoff.py, make-structured.pyNow Playing, the map, every sign-off, every page’s structured data. They read the published pages, so they run after the builders.
Site filesmake-sitemap.py, make-search-index.py, make-feed.py, make-agent-files.py, make-security.pysitemap.xml, llms.txt, cb-rooms.json, search-index.json, feed.xml, /.well-known/*
Headersmake-csp.pyThe policy line in _headers, from love-embed.js’s origin lists and the pre-paint snippet’s hash
Imagesmake-webp.py, make-og.py, make-icons.pyWebP 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 pullspull-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.pyA data file refreshed from feeds or GitHub. Never run by the build.
Intakeintake-collection.py, import-room.pyMetadata 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.

  1. The Node tests. netlify/cb/lib.test.mjs races writers against each other and fails if any write that was told it worked is missing. tools/fractal.test.mjs counts flashes in what the fractal window paints.
  2. Images, before anything refers to one.
  3. The room builders, each from its own data file.
  4. 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.
  5. The checkers, against what was just written.
CheckerRefusesRenders in Chrome
check-contrast.pyA text and ground pair under WCAG 1.4.3, and any colour on :root that is neither measured nor exempted with a reasonno
check-contrast-live.pyRendered text against the ground it actually sits on. It declines rather than guesses on a gradient.yes
check-focus.pyA focus ring that cannot be seen against its groundyes
check-gentle.pyRotation, skew or ambient motion left at Gentle, and content that changes between dial settingsyes
check-print.pyA zine page that is not one sheet, a broadside that is not two pages a sheet, an ink not on the allowed listyes, to PDF
check-weights.pyTwo of the Foundry’s weights that render the same lettersyes
check-headings.pyA skipped heading level, or other than one <h1>no
check-ids.pyAn id used twice on a page, or a script asking for an id no page hasno
check-classes.pyA class claimed by two rooms, a custom property declared twice, a section number out of orderno
check-counts.pyA sentence stating how many rooms, pages or typefaces the street hasno
check-quests.pyA job code that disagrees between a room and the Guild’s boardno
check-faces.pyA typeface in fonts/ missing from the Foundry or the Mopery’s pickerno
check-teleport.pyThe room search drifting apart between the radio, the guest bar and the finderno

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.

AreaAddressesWhat is kept, and for how long
Signing on/cb/signon, /cb/account, /cb/account/recover, /cb/account/adminA 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/imageTen 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/beaconOne 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-settingsNothing 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.xmlThe 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/adoptedA 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/moderatePresence per visit, replaced on moving. Each place’s talk is gone at midnight.
The sweepcb-midnight, scheduledDeletes 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.

ScriptRoomCommitsSubject
daily-arrivals.shThe Feeddata/arrivals.json, the-feed.htmldaily arrivals board reset
daily-scoop.shThe Doom Scoopdata/doom-scoop.json, the-doom-scoop.html, now-playing.htmldaily doom scoop edition
daily-vital.shVital Plant Living’s tellydata/vital-rack.json, vital-plant-living.htmldaily vital telly refill
daily-notebook.shThe Open Notebookdata/open-notebook-month.json, the-open-notebook.htmldaily open notebook refill
daily-large.shLearning Largedata/learning-large-month.json, learning-large.html, now-playing.htmldaily learning large refill
daily-cookin.shHey, Good Cookin’data/hey-good-cookin-month.json, hey-good-cookin.html, now-playing.htmldaily 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.

OriginUsed forAllowed byContacted when
www.youtube-nocookie.comVideos and playlists in every room with a play button, rack or setframe-src; the player’s features in Permissions-Policyafter a press
open.spotify.comClub Chronic’s stageframe-srcafter a press
embed.music.apple.comClub Chronic’s Apple Music deckframe-srcafter a press
videopress.comRyan’s own grass video in Swaying Sweetgrassframe-srcafter a press
8x8.vcA room’s voice and video callframe-src; camera, microphone, screen and autoplay in Permissions-Policyafter a press, with a token the CB signed
josephmooon.wordpress.com, its uploads folder onlyThe Small Hours’ jukebox, streamed from the band’s own sitemedia-srcafter 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:

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:

  1. Pull before touching anything; the morning task will have pushed.
  2. Run tools/check-all.sh before every commit. Files it changes are generated files catching up with a source, and they go in with the change that caused them.
  3. Commit and push when you stop. Uncommitted work blocks the other person and the morning task, which refuses to run over anybody’s edits.
  4. Never hand-merge a generated file. Take either side, fix the source and run the build again.
  5. 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 needsFor
Python 3Every generator and checker
Google ChromeThe rendering checks, the print check, icons and share cards, all headless
NodeThe local preview, the Node tests, and netlify dev for the CB
cwebp, ffmpegA new JPEG arriving
ffprobeA new recording, because runtimes are measured off the file
Pillowintake-collection.py
The Knowledge System mirrorOptional, 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.