The visitor's gallery
I run my projects out of bosun-x, and not all of them under the same name. One of those names is a business site I take client work through, and a client there wanted to see where their projects were up to. So they have a login now, to the part of bosun-x that's theirs, their projects, the tasks I've shared, the ideas they've brought me, a thread to talk in. Same bosun-x I use, filtered down and wearing that site's skin. There was no second system to build.
Not one business, one tool
I built bosun-x to keep my own projects straight, and I don’t run them all under one name. There’s x-hakt, there’s a business site I take client work through, a couple of others besides, kept apart for their own reasons. bosun-x holds the lot, every hull in the yard, in one place. One login, and it shows all of it: every project, every client, every server, and the notes I leave myself when something’s gone sideways.
Then a client. Someone I do work for through the business site, who wanted to see how their own hull was coming along without me rowing out with a report every Friday. bosun-x already had that answer, current, sitting on a page. I just couldn’t hand them that page, because it has every other hull in the yard on it as well, plus the tooling, plus the notes I write when something’s a mess.
Last time I built the part that makes it safe to show them a slice of it: one function that does the projecting, two gates a project has to pass to land in that slice, and a build rule that fails if any other client-facing code reads the store directly. That was the part I was nervous about and it went in fine. What was left was the portal itself, which I figured was a weekend of CSS. It was closer to two weeks, and hardly any of it was CSS.
It comes in through the right door
The client knows me by the business, not as x-hakt and not as a bloke with a shelf of old laptops in a spare room. So the portal wears that business’s site: the same header that starts clear and fades to a glass panel as you scroll, the same underline sliding under a nav link, the same button, the same footer. Not to fool anyone about who’s down on the yard floor. That’s the name the work is done under, and the gallery should match the yard.
Per portal it’s a short block of config. A name, two colours, a heading font, a body font, a line for the footer, a link back to the real site. Name a Google font and it loads; name something else and it falls back. Everything under that, the layout and the scroll behaviour and the motion, is the same for every portal. It isn’t a page builder. I didn’t want a second thing to maintain so that once a year someone can nudge a box.
three pixels out
Matching a real site is fussier than building one. The business site uses a plain CSS reset. The portal runs Tailwind, whose reset sets a line-height on every element, and that pushed the header nav and the sign-out button a few pixels taller than the real site. The footer was worse, every line about three pixels too tall, so the whole strip sat wrong next to the real one. Anyone with both open in two tabs would have spotted it.
The fix was one line in two places, pinning line-height back to normal on the header and the footer. Finding it took an afternoon with the two headers stacked at 400% zoom, changing a value and reloading.
Still the same tool underneath
The portal is bosun-x. The same image, started a second time in portal mode, reading the same files. The projection hasn’t changed since the note about it: the client sees a project only if it’s tagged as theirs and shared with them by name, and every field that comes back is built from an allow-list, not trimmed down from the full record.
What’s new is the part they touch. They see their projects, and inside each one the tasks I’ve shared with them. Sharing a project doesn’t share its tasks, so the one where I’ve written “the last dev did something odd with the auth flow” stays on my side. Above the tasks there’s a paragraph I write by hand about where the project actually is, in place of the line the portal used to generate from the tech stack and the status field. They can reply on any task or idea I’ve shared and it lands back on my board flagged. There’s a sign-off button. There’s a message thread for anything that isn’t tied to a project. When they come back after a week, the page opens with what moved while they were gone.
the threads showed their working
The notes on a task are one plain-text field, with --- Me · 2026-09-04 ---
header lines between the turns. The dashboard parses those into a conversation.
The portal, the first time I wired it, didn’t, so the client would have seen
the raw --- ... --- scaffolding sitting between every message. I’d reused the
storage and forgotten it needed its own reader. You’re killing me, Smalls. A
small parser on the portal side fixed it, and the client’s own replies come up
in the accent colour now, so a thread reads like a conversation and not a build
log.
Nothing to keep in sync
I never publish to the portal. No export, no build step, no button that says send update. I tick share on a project and from that point the client is reading the same file I’ve got open. A line I add to the status this afternoon is on their page this afternoon, because it’s the same line in the same file.
The obvious way to build this is a second store that you feed the safe parts into on a schedule. Then someone has to keep the two lined up, and the client’s view trails the real one by however long it’s been since anyone did. The portal reads the same files I edit, so there’s no gap for it to fall into.
How it runs
The portal is one more service in the compose file. Same image, started with
BOSUN_MODE=portal and the name of the face it should wear. It mounts the same
data directory as the dashboard, read-mostly, and the only thing it writes is a
reply into a thread. Its own domain, its own sign-in, because how a client gets
in shouldn’t have anything to do with how I do. No Docker socketThe local file (/var/run/docker.sock) that programs talk to in order to ask Docker what is running or to control it. Mounting it into a container, even read-only, is powerful — handle with care., no host paths.
Adding another face is that service block and the config values. Adding a client under it is a name, an email, and the two ticks that share their first project.
One gallery, one visitor
One portal, one client, which is all I built it for. None of it is bolted to this client or this business, though. It’s bosun-x with a gate on it and a coat of the client’s paint, so anyone running bosun-x who wants to let a client watch their own work has it already. The paint took longer than the gate.
-x