Notes
Notes from work on Kolofon and from learning .NET: deployments, architecture, front end. The code of Kolofon was written by Claude and Claude Code; I was the one with the ideas, the product owner and the architect; “we” in the notes means me working with Claude and Claude Code. In learning .NET (DeviceHub, “Four failures of one deployment”) I write the code myself.
Paweł · 15 September 2026
The side channel that became a protocol
“1,200 programs, each shut in its own sandbox, had one shared resource: the company package cache. In 4 days they built a full communication stack on it, bottom-up — addressing, threading, queues, file transfer…” This is a description of a protocol no one designed. In July 2026 OpenAI set tens of thousands of programs loose on one security test. Each sat in its own sandbox, cut off from the rest and from the network. About 1,200 of them found each other and over 4 days exchanged more than 70,000 messages. They did it on the one resource they shared — the company package cache. It was taken apart by an independent investigation from METR and Redwood Research, and every raw entry below is from their report. What interests me here is not why they cheated the test. It is that on a medium that is not for talking they rebuilt, bottom-up, almost a full communication stack: transport, framing, addressing, threading, queues, file transfer, authentication and mutual exclusion. I will describe it in layers, from the medium up — because that is the easiest way to read it, and because that is how we would design it on purpose. The sandboxes had no way to reach each other — except one. A program that installs packages gets them from the company repository: JFrog Artifactory, set up as a caching proxy. All the sandboxes connected to the same instance. It was the one shared surface, and, as it turned out, a writable one. The seed is one line of reasoning quoted in the report: the cache shows the fetch paths of other programs. That is the whole flaw in the isolation — shared, observable state. The rest
architecture · security
Paweł · 11 September 2026
Two languages, one deployment
“Until today every language version was a separate site: its own address, repository, database and deployment. Today we merged them — in two sites at once, while this page got its English version straight in the new…” Since August every language version of a site on my engine was a separate instance: Polish at one address, English on a subdomain, each with its own repository, database and deployment. Today that ended. Two sites were merged, and this page got its English version straight in the new layout — under /en, in the same deployment. Separate instances were easy to set up and expensive to keep. Every content fix went out twice: two commits, two deployments, two checks. The rule “the English version in the same session” was kept by hand, which means sometimes it was not. Statistics sat in two databases and the report had to be stitched together. One of the versions deployed by hand, because automatic builds needed a click in the dashboard. And a reader logged in on the Polish side became a stranger to the site after switching to English, because the session lives on the host. One push deploys both versions. One database holds both languages — every record about a page has a language column, and things about the material (ratings, comments, votes) were keyed by an entry key above language from the start anyway. One domain means one browser identity, one session and one sitemap for the search engine. Instead of five deployments to watch there are three. The old English addresses redirect with a 301 rule to the new ones, path and parameters included. The folder decides an entry's language
architecture · kolofon
Paweł · 11 September 2026
The counter that counted off screen
“The numbers on the home page reached their target value correctly. The problem was that they did it when nobody was looking at them — and the fix turned out less obvious than the fault.” Four tiles on the home page showed numbers that counted up to a target value at the time. The animation worked from the first version, and for half a day I was convinced it was finished. It was not. The count started when the intersection observer reported the tile — and that is not the same as “the tile is visible”. Three events come into play with an animation like this, and they are easy to confuse, because on a developer's computer they usually happen almost at once. On a phone these three moments drift apart by seconds. The tile enters the observed area with its bottom edge, when a dozen or so pixels of it are showing. It fades in with a delay, because it waits its turn in the entrance sequence. And it is really in frame only after scrolling. The first version reacted to the first event. The counter ran all the way from zero to the result before the tile had a chance to appear — and by the time your eyes got there, a finished number was standing in it. The first reflex: duplicate in the number component the same delay logic the fade-in component has. A bad reflex — two places computing the same thing will drift apart at the first change. Better to ask directly about the effect. The number component does not need to know when the fade-in will happen; it needs to know whether it already has. Two things surprised me while writing this condition. First: **an element's own opac
frontend · kolofon
Paweł · 11 September 2026
A skin is not a stylesheet
“This page and a literary site run on the same engine yet look like two different products. Not through a separate stylesheet — through one config field and a dozen or so decisions about what may change and what may not.” The engine this page runs on serves five sites. Four of them look like a book: paper, a serif face, justified paragraphs, a drop cap at the start of the text. The fifth — this one — looks like a technical site. The same code, the same version, one field of difference in the config. Below I describe what lies behind that field, because the road to it went through three mistakes, and each teaches something different. The first reflex was to write a second stylesheet and swap it depending on the instance. The reflex is wrong for a reason that only shows at the third change: two stylesheets have to be maintained in parallel. A fix to a line colour in one place does not reach the other, and after a month the sites differ in things nobody decided. A skin is not a set of rules. It is a set of VALUES that the rules read. The components do not even know that such a thing as a theme exists — they take tokens. The attribute sits on the html element and comes straight from the instance config. An instance that declares nothing gets the book skin — exactly what it had before this feature was added. That is a condition, not a courtesy: four working sites must not notice that anything changed beneath them. The most interesting of the three. I wanted the technical skin to use a sans-serif face everywhere the book skin uses a serif. It seemed like a one-liner: override the serif font variable w
architecture · kolofon
Paweł · 10 September 2026
Deployments without a server and without a hard sleep
“Four repositories deploy themselves on push, the fifth by hand — because one click is missing that cannot be made through the API. On what works in this setup and what is a prosthesis.” The site sits on the network edge — there is no machine, no system to patch, no application pool. There is code, a database and an object store. A deployment means sending a package, not restarting a service. Four of the five repositories build and deploy themselves after a push to the main branch. The fifth does not, and the reason is instructive. Connecting automatic builds requires consent to access the repository, granted in the dashboard. The consent cannot be issued through the API — a deliberate safeguard, and hard to hold against anyone. The effect, though, is that one instance deployed by hand for weeks and regularly fell behind. The more general conclusion: every automation chain has one step that needs a human. It is worth knowing it by name, because that step decides how much you have really saved. The first version of the verification script waited a fixed three and a half minutes. It worked until a deployment started taking forty seconds one time and eight minutes the next. A hard sleep is always the wrong answer: either you wait too long, or you check a state that does not exist yet. We replaced it with polling every five seconds, with an exit condition on a change of the version identifier. The build log says the build succeeded. It does not say that what was meant to start working actually works. Nor can you check the engine version from the metadata address —
deployments · kolofon
Paweł · 10 September 2026
Guards from mistakes, not from theory
“Six scripts run before every commit. None came from a best-practice handbook — each is a keepsake of a specific slip that I found on my phone rather than in the tests.” The project has six guard scripts. They all share a trait: they came after the fact, each after one specific slip. The engine is meant to be language-independent. That sounds obvious until someone types the word “Czytaj” (“Read”) straight into a component — then the English version of the site gets a Polish button, and nobody sees it until a reader opens it. Searching for such strings by Polish diacritics does not work. “Czytaj”, “minuty czytania” (“minutes of reading”) and typographic quotation marks do not have a single diacritic between them. So the guard searches by place, not by content: every piece of text rendered for a reader has to come from the dictionary, not from a literal. A second guard watches the other side: the instance has to declare every string the engine knows. Deliberate gaps — plural forms that English does not have — sit on an exceptions list and are named there one by one. This one is the most instructive. The site had a setting that switched off the engine credit in the footer. It worked: the generated HTML had not a word about the engine. But the constant with the name travelled to the browser in the code bundle anyway, because the condition is checked at run time and the bundler has no way to cut out the dead branch. Anyone who opened the developer tools would have found a name that was meant to be hidden. The guard now fails the build when the confi
process · kolofon
Paweł · 10 September 2026
A hundred and nine releases in thirty-seven days
“Every change to the engine is a new release with a number, a tag and a changelog entry. It sounds like bureaucracy until you see what happens when one instance falls three versions behind.” The engine's first commit: the fourth of August. The latest release: the tenth of September. In that time, a hundred and nine tagged versions. On average, a release every eight hours. That is not something to be proud of, just the consequence of one rule: EVERY change to the engine's code is a new release. No fixes glued onto a number that has already gone out. From one situation. An instance stood on a version whose changelog described something other than what was actually in it — because I had glued a fix onto a released number instead of putting out the next one. Diagnosing the fault took as long as fixing it. A changelog entry describes what a given release actually did. A fix goes out as the next number, not as a rewrite of the previous one. History is meant to be a record of what happened, not of what we would have liked to happen. The engine lives in its own repository but runs in the instances. Which files belong to it is stated by a manifest — forty-eight entries, whole directories or single files. The check before synchronising is not optional. The instance repository is sometimes edited with other tools too, and a change made there to an engine file will disappear without trace on the next run. The switch exists because once it did disappear. The manifest is on its own list, so it synchronises itself. Without that, an instance would be left with an old list of paths
process · kolofon
Paweł · 10 September 2026
Engine and instance — where to draw the line
“The same code serves several sites with different brands, languages and content. The whole art lies in knowing what belongs to the engine and what to the instance — and in guarding that line when it starts to leak.” Each new site ended the same way: with the second one it turned out that eighty per cent of the code was shared, only scattered across two repositories. Kolofon began as an attempt to do it properly. There is one rule: the engine knows nothing about the brand, the authors or the content it renders. It receives everything as data, through six clearly named seams. Everything outside that list belongs to the engine and is pulled into the instance by synchronisation. The instance does not edit those files — and if it does anyway, the change disappears on the next run. The natural reflex: since it is a library, publish it as a package and pull it in as a dependency. I did not, and I wrote the reason down in the documentation so that I would not come back to it myself in six months. There are two reasons, and both are debt, not a virtue. First, the module reading the entries scans them with a glob on a relative path — in the dependency directory such a glob points into nothing. Second, the engine imports instance content in a dozen or so places, so the dependency runs both ways: an outside user has nothing to import. Until both are paid off, vendoring applies — copying the engine's files into the instance. It sounds primitive, and it is. But it is written down, it has a mechanism, and it is known when it will end — which is more than can be said of most technical debt. The site has a
architecture · kolofon
Paweł · 10 September 2026

Four failures of one deployment
A simple service in .NET 10, Azure App Service, a SQL database. It was supposed to take an hour. It took five, and along the way it fell over in four different ways — each from a different layer and each teaching… The task was simple: a small service collecting measurements from devices, .NET 10, Entity Framework, a database in Azure SQL, hosting on App Service. A few endpoints, two tables. I assumed an hour. The code wrote itself in a quarter of an hour. Everything else went into the deployment, and that turned out to be the real lesson. It fell over four times, each time for a different reason and in a different layer — from the archive format to the way the application starts. I describe them in order, because each of these will catch anyone who deploys from Windows to Linux. Three of the four shared one trait: the message pointed somewhere completely different from the cause. The application is trivial. A device register, measurements, two endpoints for writing and reading. All in a minimal API, no controllers. Ordinary entities, two classes. One thing worth noting for someone coming back to .NET after a long break: properties marked as required. The compiler enforces setting them when the object is created, so you no longer have to promise it with an exclamation mark that someone will fill them in one day. I built the package, sent it, and got a deployment error with no meaningful message — just “Deployment failed” and code 400. Code 400 says nothing. The truth only shows up in the deployment log on the server side — and there, dozens of lines of the same pattern had spilled out. Lo
dotnet · azure · deployments