A desktop operating system for Southbag. The machine is a thin client, the account is the device, and there is no local user.
Milestone 1 is the boot-to-desktop moment and the shell around it. It runs in a browser because that is the fastest way to iterate on the shell, not because the OS is a website.
bun install
bun run devbun run build type-checks, builds, and enforces the bundle budget.
Lightweight is the product, not an optimisation pass. budget.json sets a
gzipped ceiling per bucket and bun run build fails if any bucket is over.
Raising a number is a tracked diff; say why in the commit message.
| Bucket | Ceiling |
|---|---|
| JavaScript | 80 KB |
| CSS | 14 KB |
| Fonts | 60 KB |
| HTML | 3 KB |
- Target runtime is WPE WebKit via
cog, not Chromium. Develop against a normal browser, but check features against WebKit before adopting them and keep polyfills out. No View Transitions API, notext-wrap: balance, nobackdrop-filter(supported, but a full-surface per-frame composite is not free on 2013 integrated graphics). - Deploys to
/os/.baseis set invite.config.ts; asset URLs inindex.htmlgo through%BASE_URL%. - No backend. State lives in
localStorage. - Fonts are self-hosted and subset. The device boots offline.
- Performance is measured on a throttled CPU and a capped memory profile, not on the development machine.
The shell does not open without a signed-in Southbag account. Sign-in is OpenID Connect against Southbag Identity (identity.southbag.cc): authorization code + PKCE, no client secret — a device cannot keep one, so the shell is a public client. Each deployment registers itself once through Identity's dynamic client registration and remembers its client id.
The id token's signature is verified against Identity's published keys before
anyone is called a customer; state, nonce, issuer, audience and expiry
are all checked. The session lives in localStorage until the token expires,
and signing out forgets it — a secretless client cannot revoke, so it forgets.
On the device the shell is served from http://127.0.0.1:8080, an origin
Identity does not CORS-trust, so registration, the code exchange and the key
fetch go through image/shell-extra/cgi-bin/identity-auth, which forwards
exactly those three calls and nothing else. The authorize redirect itself is
a top-level navigation and needs no help.
?identity=<origin> runs the flow against another Identity instance (used to
test against a local one); ?identity=reset clears the override.
Files is Southbag Drive (drive.southbag.cc), and it is the only storage in the customer journey: the system offers no local place to put files, and downloads do not work.
That is a journey constraint, not an immutable system. Nothing locks the filesystem down; Drive is simply the only thing that works:
- The image's runtime is cog, and cog has no download support: following a download link ends in a load error, so nothing reaches the disk that way. (The cog 0.18.4 that Debian trixie ships has no download handling in its source at all.)
- When the image ships Southbag Browser, the launcher marks it with
SOUTHBAG_OS=1(image/shell-extra/cgi-bin/launch) and the browser refuses downloads under that mark, with a Drive-first explanation (SouthbagHQ/browser). The standalone desktop browser never carries the mark and keeps its download manager. - Drive is the exception that proves the rule: uploading into Drive is a web flow inside the Drive window and never touches the local disk, while "downloading" out of Drive hits the same refusal as everything else. Files stay in Drive.
Off the device - the shell opened in an ordinary desktop browser - none of this applies: that browser belongs to the host, and downloads there are the host's business. Someone with the boot prompt and physical access can still do anything; that was never the threat this answers.
Most apps are the institution's existing web services, opened live at their own origins rather than re-implemented here. Nothing is framed: a service always opens as a top-level window. Off the device that is a new browser window. On the device (image/shell-extra/cgi-bin/launch) the window belongs to whichever runtime the image ships:
- Southbag Browser installed: each service opens as one of its app
windows (
southbag-browser --app=<url>) — a real top-level window with no tab strip or omnibox, on the full browser stack: the same Southbag Identity login as the shell, single sign-on into every southbag.cc service, Southbag Secure, browsing fees and Rewards. The Browser app launches the whole browser; opening a service that is already open focuses its window. - Otherwise: the session starts a cog process per app pointing at the shell's Browser web app (src/apps/Browser.svelte) — a window with a location bar that becomes the site it visits.
| App | Opens |
|---|---|
| Files | drive.southbag.cc |
| Banking | banking.southbag.cc |
| Office | office.southbag.cc |
| Chat | social.southbag.cc |
| Code | code.southbag.cc |
| Branch locator | branch-locator.southbag.cc |
| Lore | lore.southbag.cc |
| Browser | the Southbag Browser web app itself |
Terminal, Kevin and Settings have no web service and stay placeholders until they are built. The standalone Southbag Browser and Terminal products are Electron desktop applications (SouthbagHQ/browser, SouthbagHQ/terminal); images that include the browser get the app-window behaviour above, images without it keep the cog path — the OS works either way.
The shell is four surfaces, composed in src/Desktop.svelte and nowhere else.
Under labwc each is a separate layer-shell client, so nothing here may depend
on them sharing a page or being able to measure one another.
src/
Desktop.svelte composition root, and the only file that knows
the surfaces currently share a document
surfaces/
boot/ power-on to greeting
wallpaper/
panel/
platform/ identity, clock — everything an app receives
styles/tokens.css the institution's design tokens
src/styles/tokens.css extends the token set in SouthbagHQ/website. It does
not define a second design system.
The mark starts centred and ends as the panel launcher, in one continuous transform. The flight path is resolved entirely in CSS from shared panel geometry tokens, so neither surface measures the other — a layer-shell client cannot read another surface's layout, and the browser build must not rely on being able to.
Timings live in src/surfaces/boot/sequence.svelte.ts. In development,
?boot=6 stretches both the beats and the travel so the sequence can be
watched.
The ISO is built by CI on every push that touches the image or the shell, and uploaded as a workflow artifact. To build it yourself you need Linux with Docker; it cannot be built on macOS.
bun run build:device
./image/build.shThe result is out/southbag-os.iso. Write it to a USB stick — this destroys
everything on the target device, so check the identifier twice:
# Linux
sudo dd if=out/southbag-os.iso of=/dev/sdX bs=4M status=progress conv=fsync
# macOS: the r-device is the raw one, and is far faster
diskutil unmountDisk /dev/diskN
sudo dd if=out/southbag-os.iso of=/dev/rdiskN bs=4m status=progressThe ISO offers two things. Booting live runs the OS off the stick and touches nothing. The installer copies that same filesystem onto the disk, so what ends up installed is this image rather than a stock Debian reassembled from packages.
amd64, UEFI only. It will not boot on an Apple Silicon Mac, and there is no BIOS/CSM path.
Debian stable, systemd, Wayland, no X11. labwc owns compositing, input and
window geometry. The shell is a layer-shell surface; each Southbag app is its
own cog window. No display manager — a systemd user session with autologin.
systemd-boot, UEFI only. A/B partitions with dm-verity, read-only root,
systemd-sysupdate, images built with mkosi.
2013-era Intel graphics are fine on modern Mesa and Wayland. NVIDIA of that vintage on nouveau is not.