<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Adriano Foschi</title><description>Projects, experiments and lessons from 20+ years of building software — what I&apos;ve built, and what I&apos;m building now.</description><link>https://adrianofoschi.com/</link><language>en</language><atom:link href="https://adrianofoschi.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>Permissions are two problems, not one</title><link>https://adrianofoschi.com/blog/permissions-are-two-problems/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/permissions-are-two-problems/</guid><description>Every authorization engine I&apos;ve tried answers &quot;can this user see this?&quot; beautifully, and &quot;what can this user see?&quot; badly. That second question is where I keep getting stuck.</description><pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Open GitHub and the first thing in front of you is a list of repositories: the ones you can see. It looks like the most ordinary screen in the product, and it’s actually the point where the permission system has to do the hardest thing you can ask of it.&lt;/p&gt;
&lt;p&gt;Because the question that page answers isn’t “can this user see this repository?”, it’s “which repositories can this user see?”. Put that way it sounds like the same question turned around. For whoever implements it, it isn’t remotely the same: in the first case you have two concrete things in front of you, a user and an object, and you only have to decide yes or no. In the second you’re holding the user alone, and the set of possible answers is everything that exists in the system.&lt;/p&gt;
&lt;p&gt;There’s also the fact that the path making a repository visible is almost never written on the repository. You can see it because you’re a member of an organization that grants a base level of access across all its repositories; because you belong to a team the repository was granted to; because you belong to a team that is a child of that team, and access is inherited downwards; or because someone added you as a collaborator on that single repository and nothing else. Visibility is the result of inherited rules applied across several levels, and none of those levels, taken on its own, can tell you what is visible.&lt;/p&gt;
&lt;p&gt;This way of deciding access has a name: &lt;strong&gt;ReBAC&lt;/strong&gt;, relationship-based access control. What sets it apart from the more familiar models is where the information lives. Under RBAC a permission is a property of the user: you hold a role, and the role carries capabilities with it. Under ABAC it’s the outcome of comparing attributes — the user’s department, the document’s classification. Under ReBAC it’s neither: it’s a consequence of how entities are connected to each other. Nowhere is it written that you can see that repository. You can see it because a chain of relationships leads you there, and to know that, someone has to walk it.&lt;/p&gt;
&lt;p&gt;I’m currently dealing with two systems that approach this problem from opposite directions. Neither of them gives me an answer I’m happy with.&lt;/p&gt;
&lt;h2 id=&quot;the-engine-that-does-everything&quot;&gt;The engine that does everything&lt;/h2&gt;
&lt;p&gt;The first is a product where granular permissions aren’t an architectural indulgence: they’re a real requirement, and it comes from customers. Saying who is an administrator and who isn’t doesn’t cover it — you have to be able to express who sees what down to the single entity, with delegations, exceptions, and visibility that is inherited along the organizational structure and sometimes has to stop at a given level.&lt;/p&gt;
&lt;p&gt;We answered that requirement by building a model of our own. That one is ReBAC in substance too, even though we never called it that: an entity’s visibility depends on how it is connected to the structure, not on a label it carries. And the result is an engine that can express anything. It’s genuinely powerful: whatever access policy gets asked for, the configuration that produces it exists.&lt;/p&gt;
&lt;p&gt;Manageability pays the bill. Years of successive rewrites — each of which solved a real problem and left a layer behind it — have spread the logic that decides an access across too many places at once. When a bug shows up, and they do, the hard part isn’t fixing it: it’s working out where that decision was made. Nobody holds the whole system in their head, and it isn’t a matter of attention: there’s no vantage point from which you can look at all of it at once.&lt;/p&gt;
&lt;p&gt;The diagnosis I’ve settled on is that we wrote ourselves an authorization language without being a company that makes authorization. Every new requirement was answered by making the engine more expressive, because that’s the natural thing to do when the engine is yours. But expressiveness is exactly what you pay for later in maintenance: the more things a system can say, the less it’s possible to look at it and know what it is saying.&lt;/p&gt;
&lt;h2 id=&quot;outsourcing-what-isnt-the-core-business&quot;&gt;Outsourcing what isn’t the core business&lt;/h2&gt;
&lt;p&gt;The second system is a new prototype I’m building from scratch. There I wanted to try the opposite route: if writing your own permission engine is how you end up with a language to maintain, then you take authorization from outside, the way you take email delivery or authentication from outside. It isn’t my core business, and there are companies for which it is.&lt;/p&gt;
&lt;p&gt;I looked at &lt;a href=&quot;https://permify.co/&quot;&gt;Permify&lt;/a&gt;, &lt;a href=&quot;https://www.permit.io/&quot;&gt;Permit.io&lt;/a&gt; and &lt;a href=&quot;https://openfga.dev/&quot;&gt;OpenFGA&lt;/a&gt;. The serious experiments I ran on OpenFGA, because it’s the reference open-source implementation of the model Google described in &lt;a href=&quot;https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/&quot;&gt;Zanzibar&lt;/a&gt; — the system that decides access inside Google, and the paper this entire category of products grew out of. If the pattern makes sense, that’s where you’ll see it.&lt;/p&gt;
&lt;h3 id=&quot;what-the-declarative-model-gets-right&quot;&gt;What the declarative model gets right&lt;/h3&gt;
&lt;p&gt;And for a good stretch of the way it makes a great deal of sense. The authorization model is a declarative file: you write the entity types, the relationships between them, and the permissions as expressions over those relationships. The inheritance that in a homegrown engine is code scattered around is a single line there — whoever owns the container has that permission on everything the container holds, written once and true everywhere. That file lives in a repository of its own, with its tests beside it: YAML scenarios saying “this user, this permission, this entity, expected true”, running offline, with no database and no application, in a couple of seconds. I wrote sixteen of them, a hundred and twenty assertions, covering the cases nobody in the older system ever dared verify systematically: isolation between separate units, a user with no role at all, an unknown user. I didn’t even have to host it: there’s a managed service with a free tier, and for a prototype that’s enough.&lt;/p&gt;
&lt;p&gt;There’s also a discipline this separation imposes, and I think it’s the right one. Roles stay domain facts in my own database, with their invariants and their events; the external service is a projection, updated by an event handler. Which forces you to decide the order of writes. If the relationship is a domain fact, database first and service second: if the second step fails, the fact exists and the permissions arrive late, and that’s an annoyance. In the reverse order, if the database fails, you’re left with granted permissions that no longer have any fact behind them — and that isn’t an annoyance, it’s a hole.&lt;/p&gt;
&lt;p&gt;So far the experiment is a success, and that deserves saying before I get to how it ended.&lt;/p&gt;
&lt;h2 id=&quot;then-comes-the-list&quot;&gt;Then comes the list&lt;/h2&gt;
&lt;p&gt;Then comes the moment you have to build the page that lists things. It isn’t an edge case: it’s the first screen anyone who logs in sees, exactly as on GitHub. And there you discover that the service you delegated authorization to answers beautifully the question you ask about one object at a time, and very badly the one you have to ask about all of them.&lt;/p&gt;
&lt;p&gt;There are two routes, and I tried both. The first is to filter afterwards: you query the database with your normal filters and your normal pagination, take the identifiers of the page you got back, send them to the service in bulk asking which of these the user can see, and throw the rest away. It works, in the sense that the result is correct. Except that the total count you show at the bottom of the list is the one from before the filter, so it’s a lie; and a page of twenty-five items reaches the user holding whatever survived. For someone with access to nearly everything the difference doesn’t show. For a user with access to a small slice — which is the common case, not the exception — two or three of the twenty-five remain, and pagination simply stops working: you’d have to keep asking for pages until you managed to fill one.&lt;/p&gt;
&lt;h3 id=&quot;filtering-first-and-the-list-that-arrives-whole&quot;&gt;Filtering first, and the list that arrives whole&lt;/h3&gt;
&lt;p&gt;The second route is to filter first: you ask the service for the list of objects the user holds that permission on, and hand it to the database. Here the problem is that the list arrives whole. There’s no real pagination to draw twenty-five from: it gives you all of them, and you have to feed them into a clause enumerating thousands of identifiers, then sort and paginate downstream. At small numbers it’s a solution. At real numbers it’s a query nobody wants in production.&lt;/p&gt;
&lt;p&gt;Against this objection there’s an encouraging figure in circulation, a public case study reporting a page that went from ten seconds to four hundred milliseconds with this approach. I read it carefully because I wanted it to be true, and it doesn’t say what it appears to say: the bulk filter accounts for a factor of two, eight seconds to four. The big jump comes from something else — moving the database into the same region as the service. That’s a networking fix, not an answer to the problem.&lt;/p&gt;
&lt;h2 id=&quot;the-materialization-deadlock&quot;&gt;The materialization deadlock&lt;/h2&gt;
&lt;p&gt;At this point there’s only one obvious move, and it’s the one everybody makes: if you can’t filter at runtime, you build a local index. A table in your own database saying, for each user, what they can see. From there on the list goes back to being an ordinary query, with its pagination and its count, because the filter has become a JOIN.&lt;/p&gt;
&lt;p&gt;The problem is how you fill that table. What you need are the &lt;em&gt;computed&lt;/em&gt; permissions — “this user can see this object” — and the external service doesn’t give you those. It has an API for following changes, but it returns relationships, that is, the facts you wrote into it: that you’re a member of that team, that the team has access to that repository. That’s data you already have. The interesting part is what the engine derives by walking the graph, and that part it keeps to itself: it computes it to answer one question, then discards it.&lt;/p&gt;
&lt;p&gt;To get it out you have to ask, and asking means one question per object. Thousands of questions per user, to be redone every time anything changes anywhere along the chain. It’s the same wall as before, moved inside the synchronization process.&lt;/p&gt;
&lt;h3 id=&quot;where-each-service-stops&quot;&gt;Where each service stops&lt;/h3&gt;
&lt;p&gt;Permify has the better API on this front — a reverse lookup starting from the subject’s relationships, with cursor pagination and a streaming variant — and for a moment it looks like the answer. But it returns the objects of one type at a time, and a complete index wants all of them: the organizations, then the repositories of those organizations, then what lives inside those repositories. The cardinality explodes the same way. The mechanism changes, the bottleneck doesn’t.&lt;/p&gt;
&lt;p&gt;Permit.io is the only one attacking the problem at the root: partial evaluation of the policy, which instead of a list of identifiers hands you a clause to drop into a &lt;code&gt;WHERE&lt;/code&gt;. That’s exactly the right thing. Only, for Postgres it’s in early access, it works for attribute-based policies rather than relationship-based ones — that is, not for the model I have — and it requires writing the rules in a separate language. Mature materialization does exist, but as commercial products in early access, or on stacks that aren’t mine.&lt;/p&gt;
&lt;img alt=&quot;Four independent paths by which a user can reach a repository: organization membership granting a base permission, a grant to a team, inheritance from a parent team, and a direct collaborator grant on a single repo&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/permissions-are-two-problems-0.svg&quot; width=&quot;653&quot; height=&quot;669&quot; /&gt;
&lt;p&gt;&lt;em&gt;Every arrow leaving &lt;code&gt;@user&lt;/code&gt; is a different path to the same kind of object. To answer “can they see repo A?” one path reaching the destination is enough. To answer “which repositories can they see?” the walk has to be repeated for every repository that exists — and none of the repositories knows it is reachable.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-limit-is-structural-not-an-implementation-detail&quot;&gt;The limit is structural, not an implementation detail&lt;/h2&gt;
&lt;p&gt;Lined up together, every attempt fails at the same point, and it’s a point that doesn’t depend on the product I picked. Filtering downstream breaks counts and pagination. Filtering upstream doesn’t compose with SQL. The local index needs computed permissions the engine doesn’t expose. Generating SQL clauses, which would be the right answer, only exists for a policy model different from mine. Changing tools changes the name of the API, not the outcome.&lt;/p&gt;
&lt;p&gt;The reason lies in the shape of the problem itself. These engines answer by walking a graph from a point: give me a user and an object, and I’ll find whether a path joins them. It’s a targeted operation and that’s why it’s extremely fast, milliseconds. The inverse question isn’t the same operation reversed: it’s the same operation repeated for every candidate object, because there’s nowhere that records which objects are reachable. Which is, incidentally, exactly the thing that makes ReBAC convenient — visibility isn’t a label on anything, it’s a consequence — seen from the other side.&lt;/p&gt;
&lt;p&gt;That’s where, for me, the experiment closes. Inside those systems there’s logic that knows how to answer the right question, and it’s powerful, declarative, tested by me with a hundred and twenty assertions. But it isn’t lendable. There’s no way to tell it “apply yourself to everything and tell me what comes out” in a form I can put in a &lt;code&gt;WHERE&lt;/code&gt;. So, to get the list, that logic is something I have to rewrite: in my database, in my language, from scratch.&lt;/p&gt;
&lt;p&gt;And if I write the same rule twice in two different places, those two definitions have to stay in agreement forever. The worse part is which of the two actually counts: the copy, the one I rewrote, is what decides what people see on screen. The original only guards the writes. If they drift apart nobody notices, because a wrong list doesn’t raise an error — it just has one row too few, or one too many.&lt;/p&gt;
&lt;h2 id=&quot;giving-up-and-going-back-to-postgres&quot;&gt;Giving up, and going back to Postgres&lt;/h2&gt;
&lt;p&gt;In the prototype I removed the external service and put authorization inside Postgres, as native functions. It wasn’t the conclusion I wanted — the stated goal was to stop writing myself a permission engine at home — but between two copies of the same rule and a single copy, the single copy wins.&lt;/p&gt;
&lt;p&gt;The functions come in two families. One for point checks, one per write operation that needs authorizing: they take the user and whatever they’re acting on, and answer true or false. The other for filtering: they take the user and return a set of identifiers, the ones they’re allowed to see. Translated into the vocabulary of the earlier example, the list of repositories becomes this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; r.&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; repositories r&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; IN&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; authorized_repositories(:user_id))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  AND&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;archived&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;ORDER BY&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; r&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;pushed_at&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; DESC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;LIMIT&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 25&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; OFFSET &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is the thing I’d been looking for from the start: a subquery like any other, composing with the filters that were already there, with the sort and with the pagination. The total count is an ordinary &lt;code&gt;COUNT(*)&lt;/code&gt; and it tells the truth. Check and filter read the same data in the same transaction, so there’s no window in which permissions lag behind facts — because there’s nothing to synchronize, there’s no second place.&lt;/p&gt;
&lt;p&gt;The move cost less than expected, for a reason worth stating: the application had never known about the external service. Use cases called a port with a deliberately generic signature — user, permission, context — and behind that port sat an adapter. Replacing it with one that runs the SQL functions didn’t require touching any application logic.&lt;/p&gt;
&lt;p&gt;And what convinces me most is how it grows. If tomorrow I need a permission that doesn’t flow through the structure but is granted directly on a single entity, you add an assignment table, an &lt;code&gt;OR&lt;/code&gt; clause in the check function and the same clause in the filtering one. One more level of granularity costs three changes, always the same three, and they’re SQL. Linear growth, rather than an engine that gets a little more expressive every time.&lt;/p&gt;
&lt;h3 id=&quot;three-open-problems-and-the-last-one-is-serious&quot;&gt;Three open problems, and the last one is serious&lt;/h3&gt;
&lt;p&gt;That said, I don’t want to make it look like a clean solution, because it has three open problems and I consider the last one serious.&lt;/p&gt;
&lt;p&gt;The first is that I don’t know how far it holds. The functions run, and on the prototype they’re instant — but a prototype doesn’t carry a real customer’s data volume, and I haven’t measured anything under load yet. At some point a filter recomputed on every query will stop being free, and I don’t know where that point is. Finding it is the next thing to do: until I have, everything above is a decision made on reasonable grounds, not on measurements.&lt;/p&gt;
&lt;p&gt;The second is what I lost by going back inside the database. The declarative model was readable: one file, the types, the relationships, the permissions as expressions over those relationships. And its tests ran in two seconds with nothing running, a hundred and twenty assertions stating in black and white who sees what. Now the access policy is code inside the database, versioned through migrations and verifiable only against a live database. It works better and it’s harder to look at. It’s a trade I made deliberately, but it’s still a trade.&lt;/p&gt;
&lt;p&gt;The third is the paradox this whole story runs into. The prototype can afford native functions because its model is simple: a few roles, a few dozen combinations, a two-level hierarchy. If it grew as complex as the other product’s, this decision would have to be re-evaluated from scratch — meaning I’d find myself exactly where I started, with homegrown authorization logic that widens with every requirement. The simple system can do without an external engine, and indeed it just got rid of one. The complex system, the one that would genuinely benefit from an external engine, is precisely the one that gets least out of it: it has the deepest hierarchies, the longest lists and the largest volumes, which are exactly the three things these tools don’t help with. Whoever can afford them doesn’t need them; whoever needs them can’t afford them.&lt;/p&gt;
&lt;h2 id=&quot;the-compromise-making-the-question-smaller&quot;&gt;The compromise: making the question smaller&lt;/h2&gt;
&lt;p&gt;Then there’s a third possibility, one I haven’t taken all the way yet but that I’m coming round to — and that I suspect is what everyone does without writing it down anywhere. It doesn’t consist of answering the hard question better. It consists of never asking it in that form.&lt;/p&gt;
&lt;p&gt;Seen from the outside, the screen that lists &lt;em&gt;everything&lt;/em&gt; a user can see doesn’t exist, in any product. GitHub shows you an account’s repositories, or an organization’s; search works within a scope. There is no page promising the complete set of what you have access to, sorted by date. If that page doesn’t exist, maybe it’s because nobody ever wanted it enough to pay for it — and I was treating it as a requirement only because it’s easy to state.&lt;/p&gt;
&lt;p&gt;Technically this changes one thing, but it changes all of it. If every query carries a mandatory scope — this organization, this owner, this period — the candidate set stops being “everything that exists” and becomes a number I can bound contractually. And if that number is small enough, downstream filtering becomes viable again: I don’t filter a page, I filter the whole set and paginate &lt;em&gt;after&lt;/em&gt;. The count is computed over the survivors, so it’s true, and the pages are full. What didn’t work in the earlier section wasn’t filtering afterwards: it was filtering after having already paginated.&lt;/p&gt;
&lt;h3 id=&quot;what-the-scope-constraint-costs&quot;&gt;What the scope constraint costs&lt;/h3&gt;
&lt;p&gt;The price is honest and worth stating. It’s a product constraint dressed as a technical decision, and it holds only as long as I control the surface: a generic query API is something I couldn’t afford. I lose exactly the questions that have no scope — global search, exporting everything a user can see, cross-cutting counters of the “you have forty-seven items expiring” sort. The bound on candidates has to be guaranteed rather than hoped for, because the customer with the twenty-thousand-object organization does eventually turn up, and you need a hard limit with defined behaviour for when it’s crossed. And on every request I pay for a bulk check across the whole scope: latency, and on a metered service a line item too.&lt;/p&gt;
&lt;p&gt;The fact remains that this isn’t an answer to the question: it’s a negotiated surrender, making the question small enough that the tool I have can handle it. And the longer I look at it, the less it feels like a defeat. “Which repositories can user X see” isn’t a question that arises from a need — it arises from how we designed the screen. The real need is always narrower: which repositories in this organization, which ones touched recently, which ones match what I’m searching for right now. And narrower questions are ones the tools I tried answer extremely well.&lt;/p&gt;
&lt;p&gt;I’m not there yet. Today the prototype filters in SQL, and for what it has to do that’s fine. But if I had to bet on where the answer lies, I wouldn’t bet on an engine that learns to answer the big question. I’d bet on no longer needing to ask it.&lt;/p&gt;
</content:encoded><category>architecture</category></item><item><title>Same word, two different jobs: sysadmin before and after AWS</title><link>https://adrianofoschi.com/blog/sysadmin-before-and-after-aws/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/sysadmin-before-and-after-aws/</guid><description>I was a systems administrator twice, a decade apart — and the second time, on AWS, almost nothing from the first job carried over.</description><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;During my last years at university, and for a while after graduating, I worked as a systems administrator for a small hosting company, to pay for my studies. The job was simple enough to describe in full: I managed the customer control panel, answered support tickets, and whenever a client needed a new environment I set one up — almost always a LAMP(P) stack, Linux/Apache/MySQL/PHP, sometimes with something on top for managing the database. I configured small Linux boxes, one at a time, following more or less the same pattern every time. None of it was complex. I never really thought of myself as “a systems administrator” for that job — it was closer to running through a checklist than designing anything.&lt;/p&gt;
&lt;h2 id=&quot;a-completely-different-job&quot;&gt;A completely different job&lt;/h2&gt;
&lt;p&gt;From 2019, the work changed completely. I started building product — an initiative that began inside the company I worked for and would later spin out as an independent startup in 2023 (4HSE) — and for years that work had nothing to do with servers, tickets, or control panels. It was product development: features, application architecture, the kind of problems you solve in code, not in the machine running it. The systems administrator I’d been to pay for my studies felt like it belonged to a different life.&lt;/p&gt;
&lt;h2 id=&quot;the-same-word-on-aws&quot;&gt;The same word, on AWS&lt;/h2&gt;
&lt;p&gt;Then, after years of purely product-focused work, I found myself doing — in small doses — what I would have called “sysadmin work” a decade earlier. Only this time the terrain was completely different: EC2, ECS, Docker containers, CloudFront, RDS, S3, Lambda, CloudWatch. Same label as the first job — “I handle the infrastructure” — but almost no real overlap with what I used to do at the hosting company.&lt;/p&gt;
&lt;h2 id=&quot;why-were-these-never-the-same-job&quot;&gt;Why were these never the same job?&lt;/h2&gt;
&lt;p&gt;The core of the difference isn’t scale, it’s the nature of the work itself. At the hosting company, “being a systems administrator” meant keeping a machine alive: installing packages one by one, configuring Apache and MySQL by hand on that specific box, and when something broke, opening a ticket and fixing it right there, on that instance. I didn’t declare the database, I installed and maintained it myself. I didn’t design the network, I inherited whatever was already there. On AWS, that same kind of responsibility is almost an entirely different exercise: I don’t install a database, I declare an RDS instance and let it be managed; the application doesn’t run on a machine I maintain package by package, it runs in a Docker container orchestrated by ECS; I don’t inherit the network, I design it — VPCs, security groups, who’s allowed to talk to whom. The job isn’t “keep a machine alive” anymore, it’s “compose managed services that keep themselves alive.” It’s the same word, systems administrator, applied to two jobs that barely touch.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;At the hosting company&lt;/th&gt;
&lt;th&gt;On AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The database&lt;/td&gt;
&lt;td&gt;Installed and maintained by hand, on that box&lt;/td&gt;
&lt;td&gt;Declared as an RDS instance, and managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the application runs&lt;/td&gt;
&lt;td&gt;A Linux box kept alive package by package&lt;/td&gt;
&lt;td&gt;A container orchestrated by ECS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The network&lt;/td&gt;
&lt;td&gt;Inherited, whatever was already there&lt;/td&gt;
&lt;td&gt;Designed: VPCs, security groups, who may talk to whom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The job, in one line&lt;/td&gt;
&lt;td&gt;Keep a machine alive&lt;/td&gt;
&lt;td&gt;Compose managed services that keep themselves alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The skill&lt;/td&gt;
&lt;td&gt;Mechanical&lt;/td&gt;
&lt;td&gt;Architectural&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;What changed wasn’t just my own experience — the job itself changed. Platforms like AWS didn’t make the systems administrator’s work easier, they made it a different job. In my case, keeping a machine alive by hand and composing managed services that keep themselves alive have almost nothing in common as skills — one is mechanical, the other architectural. That’s probably why, the second time I found myself doing it, it didn’t feel like going back to the first job — it felt like a new one.&lt;/p&gt;
</content:encoded><category>career</category></item><item><title>The cost of writing collapsed. The cost of checking didn&apos;t</title><link>https://adrianofoschi.com/blog/cost-of-checking/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/cost-of-checking/</guid><description>Obrussa is an approach I&apos;m trying right now: treating AI-assisted development as a system problem — explicit context, a blocking gate, durable state — because the bottleneck moved from producing code to verifying it.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is an approach I started building in 2026 and am still in the middle of. It has no results to report yet — no before-and-after numbers, no verdict. I’m writing it down because the reasoning behind it is the part I’d want to read from someone else, and because committing to it in public makes it harder for me to quietly move the goalposts later.&lt;/p&gt;
&lt;p&gt;The short version: getting an AI agent to write code well is not mainly a prompting problem. The leverage isn’t in the prompt. It’s in the system that decides what the agent works on, what gate verifies the result, and what state survives between one run and the next. So instead of collecting prompting tricks, I’ve been building the system.&lt;/p&gt;
&lt;h2 id=&quot;why-checking-not-writing-is-now-the-bottleneck&quot;&gt;Why checking, not writing, is now the bottleneck&lt;/h2&gt;
&lt;p&gt;Producing code has become dramatically cheap. Checking it has not.&lt;/p&gt;
&lt;p&gt;That sentence is the whole argument, so it’s worth being precise about why the second half is true. A model can hand me a coherent 400-line change in under a minute. Reading that change properly costs me exactly what it cost before — arguably more, because the failure mode has changed. Obviously-wrong code announces itself. Plausible-but-wrong code doesn’t: it compiles, it reads like something a competent person wrote, it passes the tests that exist, and it’s wrong in a way that requires knowing the intent to see. Volume went up, and the per-unit cost of verification went up with it.&lt;/p&gt;
&lt;p&gt;So the bottleneck moved. The scarce resource is no longer typing, it’s attention — and attention doesn’t scale by adding agents. If the cost of production drops by an order of magnitude and the cost of verification doesn’t, then verification is where the whole thing either works or quietly fails.&lt;/p&gt;
&lt;p&gt;There’s an obvious move here that doesn’t work: have a second agent review the first one’s code. I use maker/checker separation, but on its own, without an objective check underneath it, that arrangement is two optimists agreeing with each other. The model that wrote the code is a lenient judge of it, and a second model with no fixed standard to check against tends to find the work acceptable too. Agreement is not verification.&lt;/p&gt;
&lt;p&gt;The practical consequence is that &lt;strong&gt;“done” has to be measurable rather than a judgement&lt;/strong&gt;. Which means as much verification as possible has to stop being an act of reading and become an act of running.&lt;/p&gt;
&lt;h2 id=&quot;introducing-obrussa&quot;&gt;Introducing Obrussa&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/adrianofoschi/obrussa&quot;&gt;Obrussa&lt;/a&gt;&lt;/strong&gt; is a public repository of standards, architecture decision records and templates that every project of mine inherits, and it contains no product code — MIT licensed, so the template can be copied into anything, including closed source. The word is Latin, from the Greek &lt;em&gt;obryza&lt;/em&gt;: the assaying of gold by fire. &lt;em&gt;Aurum ad obrussam&lt;/em&gt; is gold refined to the highest purity, and Seneca uses &lt;em&gt;ad obrussam&lt;/em&gt; figuratively for something that has been put to the test and proved genuine. It seemed like the right name for a system whose entire job is telling apart what is correct from what merely looks correct.&lt;/p&gt;
&lt;p&gt;In concrete terms: architecture decision records covering the framework itself, a document on how to keep many repositories aligned without duplicating or contradicting each other, and a service template — the skeleton a new repo starts from, carrying a deliberately thin context file, the gate specification, a state file, a contract, and an empty skills folder waiting to be earned.&lt;/p&gt;
&lt;p&gt;At the top of it sits the principle everything else is derived from: the leverage is no longer in the prompt, it’s in the system that decides what the agent works on, what gate verifies the result, and what state survives between runs. Everything below is what that turns into when you make it concrete.&lt;/p&gt;
&lt;h2 id=&quot;two-tiers-split-by-what-happens-when-a-check-is-wrong&quot;&gt;Two tiers, split by what happens when a check is wrong&lt;/h2&gt;
&lt;p&gt;Tests check what you thought to check. That covers the code compiling, the happy path working, the cases someone imagined. What tests systematically miss is a different category:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;security that isn’t a functional bug — missing authorisation, an injection point where the happy path passes cleanly, careless secret handling;&lt;/li&gt;
&lt;li&gt;contradictions between modules — A assumes X, B assumes not-X, and each passes its own tests;&lt;/li&gt;
&lt;li&gt;code that is internally coherent and wrong with respect to the intent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These don’t announce themselves per change. They accumulate invisibly and surface together, late, which is the most expensive moment available.&lt;/p&gt;
&lt;p&gt;My first attempt at the gate sorted the checks into four levels: form, security, coherence, licensing. It looked tidy and it was the wrong cut, because it answered &lt;em&gt;what gets checked&lt;/em&gt; and left open the only question that turns out to matter — &lt;strong&gt;what must never be left to a judgement&lt;/strong&gt;. Those are different questions, and only the second one tells you where to spend.&lt;/p&gt;
&lt;p&gt;So the gate has two tiers, and an invariant is assigned to one by the consequence of getting it wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tier one is deterministic and blocking&lt;/strong&gt;, and it exists only for invariants where a false negative is catastrophic and irreversible. In most systems that list is short, and writing it down explicitly is most of the work: cross-tenant data access, a secret reaching a commit, something private crossing into something public, a licence boundary that may only be crossed one way. For these, a probabilistic guarantee is not a guarantee. They get fitness functions on the import graph, isolation tests, secret and dependency and licence scanning, strict types and lint. &lt;strong&gt;Never an LLM alone&lt;/strong&gt; — a model may review them in addition, never instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tier two is an LLM reading the standards as its rubric&lt;/strong&gt;, and it covers everything that resists codification: architectural intent, whether a write really goes through a properly scoped use case, naming coherence, replay safety. A different model from the one that wrote the change.&lt;/p&gt;
&lt;img alt=&quot;How an invariant is assigned to a tier: if a false negative would be catastrophic and irreversible it goes to the deterministic tier, and everything else goes to the tier where a model reads the standards as its rubric&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/cost-of-checking-0.svg&quot; width=&quot;815&quot; height=&quot;567&quot; /&gt;
&lt;p&gt;&lt;em&gt;The cut is by consequence, not by subject. The first attempt sorted checks by what they
looked at, which left the only question that decides where to spend unanswered.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The rule fits on one line:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Catastrophic and irreversible → deterministic. Everything else → the rubric.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two things follow from that, and they’re the parts I’d defend hardest.&lt;/p&gt;
&lt;p&gt;The first is that &lt;strong&gt;the prose is the prompt&lt;/strong&gt;. The reviewing model reads the standards documents themselves — not a checklist derived from them. Nothing gets translated, so there’s no second artefact to drift out of sync, and the standards earn their token cost twice: once as the rules a person argues with, once as the rubric a model applies. It also disciplines the writing, because a rule too vague to be applied by a model was too vague to be applied by a person and nobody had noticed.&lt;/p&gt;
&lt;p&gt;The second is a small operational rule with a nasty failure mode behind it. A change to a standard that touches a deterministic check has to land in the &lt;strong&gt;same pull request&lt;/strong&gt; as the change to its config. Otherwise the code stays green while obeying the rule you just replaced, and prose and enforcement part company silently. The gate guarantees code follows the standard; that rule is what keeps the standard and its enforcement together, and nothing automated catches it.&lt;/p&gt;
&lt;p&gt;What the two tiers have in common is the same purpose as before: converting as much as possible into something CI can fail. What CI fails, nobody has to notice.&lt;/p&gt;
&lt;h2 id=&quot;the-number-that-decides-whether-this-works&quot;&gt;The number that decides whether this works&lt;/h2&gt;
&lt;p&gt;The metric I’m tracking is &lt;strong&gt;cost per accepted change&lt;/strong&gt;: what it takes, end to end including my own review time, to get one change actually merged. Not lines produced, not tokens spent, not how impressive the diff looked.&lt;/p&gt;
&lt;p&gt;That number is also the falsification condition, which matters to me more than Obrussa does. Below a certain acceptance rate, AI leverage is negative — you’re paying in review attention more than you’re saving in production. If that’s where the number lands, the honest response is to say so rather than to keep adding process. I’d rather have a metric that can tell me I’m wrong than a conviction that can’t.&lt;/p&gt;
&lt;p&gt;And there is an irreducible share of human judgement that no gate absorbs. I handle it by risk-tiering rather than by pretending: authentication, payments, personal data and security boundaries get read line by line, by me. Everywhere else, the gate plus spot-checks is the deal. That’s an explicit trade, and writing it down is what makes it a trade instead of a drift.&lt;/p&gt;
&lt;h2 id=&quot;three-kinds-of-memory-and-one-thats-deferred&quot;&gt;Three kinds of memory, and one that’s deferred&lt;/h2&gt;
&lt;p&gt;The other half of Obrussa is what an agent knows when it starts. Three tiers, separated by cost and by when they load:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always-on context&lt;/strong&gt; — vision, architecture, domain, threat model, the standards that apply, and the repo’s &lt;code&gt;CLAUDE.md&lt;/code&gt;. This describes &lt;em&gt;what the system is&lt;/em&gt; and what the rules are. It loads every session, so it’s paid for in tokens every session, so it has to stay thin. It’s also the tier-two rubric, which is the argument for paying for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; — a procedure for something recurring, loaded on demand, able to carry scripts and reference material. This is where repeatable know-how goes precisely so it doesn’t bloat the always-on context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic state&lt;/strong&gt; — a &lt;code&gt;STATE.md&lt;/code&gt; saying where the work currently is, updated as slices land.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rule that keeps them apart: if it’s &lt;em&gt;always true about the project&lt;/em&gt;, it belongs in context; if it’s &lt;em&gt;how you do X when you need X&lt;/em&gt;, it belongs in a skill. Putting procedures in the always-on file is the token waste to avoid.&lt;/p&gt;
&lt;p&gt;One thing I deliberately left out: a vector store. Product memory (RAG over a corpus) and coding-agent memory are different problems, and for the second one the codebase plus git plus native search already is the semantic memory. Adding retrieval infrastructure here would be solving a problem I don’t have.&lt;/p&gt;
&lt;p&gt;Skills are also deliberately deferred. The empty &lt;code&gt;skills/&lt;/code&gt; folder ships in the template with a candidates file next to it, and nothing gets promoted into a skill until at least one manual run of that procedure has proved reliable. A skill written before that is an unverified hypothesis with the authority of documentation — it crystallises a convention nobody has tested. Order matters: foundations, then the gate, then the template, then a first service built in vertical slices under the gate, then skills extracted from whatever actually held.&lt;/p&gt;
&lt;h2 id=&quot;many-repos-and-the-context-you-give-up&quot;&gt;Many repos, and the context you give up&lt;/h2&gt;
&lt;p&gt;The layout I use is hub and spoke: one private repo holding system-level documents, standards, versioned contracts and a service catalogue, plus one repo per service. That buys clean licence and visibility boundaries and costs something specific: an agent working in one repo cannot see the others. It will happily make an assumption about a neighbour that used to be true.&lt;/p&gt;
&lt;p&gt;The substitute for the unified context you gave up is contracts published as versioned artefacts, with consumer/provider contract tests in the gate. That’s the machine-checkable replacement — it doesn’t inform the agent, it &lt;em&gt;fails&lt;/em&gt; when the agent gets it wrong, which is the same outcome one step later. And the tedious job of keeping N repos on the same version of shared conventions is repetitive and machine-checkable, which makes it the honest place for an automated maintenance loop: a bot opening pull requests across repos when the standards change. Loops earn their place there, in maintenance, not in greenfield work where nobody can say what “correct” means yet.&lt;/p&gt;
&lt;h2 id=&quot;where-obrussa-actually-stands&quot;&gt;Where Obrussa actually stands&lt;/h2&gt;
&lt;p&gt;Today it is a boilerplate and a set of decisions, being applied for the first time. The parts I believe most are the asymmetry argument and the insistence that “done” be measurable — those hold regardless of tooling. The part I’m least sure about is proportion: whether a solo developer can carry a deterministic tier plus contract tests plus a review pass without the ceremony eating the gain it was supposed to protect. Splitting the gate by consequence helps here more than it first appears — the expensive tier is the deterministic one, so keeping its list of invariants short is exactly what makes the thing affordable. That’s exactly what cost per accepted change is there to answer, and it’s a number I don’t have yet.&lt;/p&gt;
&lt;p&gt;What I’m confident about is the shape of the problem. When producing a change becomes nearly free and verifying it doesn’t, everything that matters moves to the verification side. Prompting better doesn’t touch that. Building a system where correctness is checked by something that never gets tired, and where human attention is spent deliberately on the parts that deserve it, might.&lt;/p&gt;
</content:encoded><category>ai</category><category>architecture</category></item><item><title>Architecture as a standard, not a suggestion</title><link>https://adrianofoschi.com/blog/architecture-as-a-standard/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/architecture-as-a-standard/</guid><description>Where AI-assisted coding actually fails isn&apos;t algorithms, it&apos;s architectural boundaries. So I stopped leaving structure to the model: hexagonal clean architecture on NestJS, with the dependency rule enforced on the import graph and a composition root that is the only place allowed to know every piece.</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Since the AI era began, every one of us developers has watched our value as programmers depreciate: writing code was the skill the job rested on, and it has become the part a machine does in seconds. Ask for a feature and you instantly get hundreds of lines — the cost of writing is essentially gone. But that cost doesn’t vanish, it moves to checking, and checking is more expensive than writing: reviewing code you didn’t write is tiring, and reviewing mountains of it much more so. There’s no escape in verifying step by step either, because you don’t know up front where the model is heading — you’re judging a move without knowing the game. That’s where vibe-coding starts: not out of laziness, but because at some point genuinely verifying costs more than accepting and hoping.&lt;/p&gt;
&lt;h2 id=&quot;where-does-ai-assisted-coding-actually-fail&quot;&gt;Where does AI-assisted coding actually fail?&lt;/h2&gt;
&lt;p&gt;There is one thing I’ve noticed with some consistency, though: the point where these tools fail isn’t implementation. Ask for an algorithm, a transformation, a complicated query, and it comes out correct — that’s exactly the kind of closed problem they’re strong at. What comes out arbitrary are the architectural boundaries: where to put a file, which layer may know which other, how information should travel between layers, what belongs to the domain and what to infrastructure.&lt;/p&gt;
&lt;p&gt;The framework alone doesn’t fix it. A framework gives you bricks and a few conventions, but it isn’t rigid enough: it leaves too many roads open, all equally plausible. And when every road is plausible, the choice becomes arbitrary — not wrong, random. Two similar features end up structured two different ways, and neither violates any rule, because the rule doesn’t exist.&lt;/p&gt;
&lt;h2 id=&quot;giving-the-ai-a-pattern-to-treat-as-scripture&quot;&gt;Giving the AI a pattern to treat as scripture&lt;/h2&gt;
&lt;p&gt;The most effective lever I’ve found is removing that ambiguity up front: hand over a precisely documented design pattern and require it be treated as scripture, not as a suggestion. If the organization of the code is already decided, it isn’t something the model has to invent — and the space in which it can go wrong shrinks to what it’s actually good at.&lt;/p&gt;
&lt;p&gt;The catch is that this only works if you know that pattern, and know it well. I should be honest about my starting point: I’ve built a lot of experience on complex, highly scalable systems, but I built it in the field, almost never approaching the material theoretically. I knew how to make things work without always being able to name the principle I was applying — which is probably the norm among experienced developers rather than the exception. To have a model put a principle into practice, though, I need to know it with a precision I never needed before: I have to be able to write it down, justify it, and tell the case where it holds from the case where it doesn’t. That’s the unexpected gain of this period — the machine writes the code, I have to know the theory, and better than before.&lt;/p&gt;
&lt;h2 id=&quot;the-choice-hexagonal-clean-architecture&quot;&gt;The choice: hexagonal clean architecture&lt;/h2&gt;
&lt;p&gt;The pattern I started studying most seriously is hexagonal clean architecture. Let me say it immediately: for many projects it’s overkill, and I have no intention of arguing otherwise. The ceremony it demands — ports, adapters, a layer that isn’t allowed to talk to another — is hard to justify on a small service that just needs to work.&lt;/p&gt;
&lt;p&gt;But it’s effective precisely where it’s needed here, for three reasons. First: it doesn’t leave the model any choice about how to organize the code, because the structure is decided before a line is written. Second: it states explicitly how information travels between layers, which is the other half of what goes arbitrary when a rule is missing. The third matters with no AI involved at all: separating the domain from technical details preserves the application’s ability to evolve, and an application that can replace pieces without rewriting itself is an application that lasts.&lt;/p&gt;
&lt;p&gt;As a framework I picked NestJS, which comes with modules and dependency injection out of the box: the layers have something to rest on instead of having to be simulated. Everything that follows is the rigidity I added on top.&lt;/p&gt;
&lt;h2 id=&quot;four-layers-and-the-dependency-rule&quot;&gt;Four layers, and the dependency rule&lt;/h2&gt;
&lt;p&gt;There are four layers and the dependencies all point inward. At the centre, the &lt;strong&gt;domain&lt;/strong&gt;: entities, value objects, events, and zero framework imports — no decorators, no database driver, no notion that HTTP exists. Around it the &lt;strong&gt;application&lt;/strong&gt;, which depends only on the domain, orchestrates the use cases and declares the &lt;strong&gt;ports&lt;/strong&gt;, the interfaces describing what it needs from outside. At the edge, two families of adapters: &lt;strong&gt;infrastructure&lt;/strong&gt; implements those ports against the real world — repositories, HTTP clients, providers — and &lt;strong&gt;presentation&lt;/strong&gt; is the way in: controllers, DTOs, CLI, translating the outside world into use-case calls.&lt;/p&gt;
&lt;p&gt;The hard rule is that inner layers never import outer ones. The domain stays testable with no database and no framework, and database, framework and entry channel become replaceable details — that’s the invariant of all clean architecture, not a preference of mine.&lt;/p&gt;
&lt;p&gt;There is one part of that rule I care about more than the rest, because it’s the part most often forgotten: &lt;strong&gt;infrastructure and presentation do not import each other&lt;/strong&gt;. They’re siblings at the edge, not one stacked on the other, and whoever composes them is what puts them in touch. A controller that could import a repository directly would have a road around the use case — and that road would get taken, if not by me then by an agent looking for the shortest path from request to database. Forbidding it in both directions is what makes the use case the only way through instead of the recommended way through.&lt;/p&gt;
&lt;img alt=&quot;The four layers with every dependency pointing inward: presentation and infrastructure sit side by side at the edge, both depending on the application, which depends on the domain, and with no connection of any kind between the two edge layers&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/architecture-as-a-standard-0.svg&quot; width=&quot;555&quot; height=&quot;509&quot; /&gt;
&lt;p&gt;&lt;em&gt;Dependencies point inward, and the two edge layers are siblings: a controller reaching a
repository directly would be a road around the use case.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;ports-owned-by-the-consumer-and-the-composition-root&quot;&gt;Ports owned by the consumer, and the composition root&lt;/h2&gt;
&lt;p&gt;The piece that took me longest to genuinely understand isn’t the pyramid of layers, it’s where ports and their adapters live.&lt;/p&gt;
&lt;p&gt;A port is always declared &lt;strong&gt;where it is consumed&lt;/strong&gt;, never where it is implemented. The consumer states what it needs — an interface saying “I need to be able to save a subscriber”, written in the application layer of the module that has to save one — rather than the provider announcing what it offers. It looks like a detail about where files go, and it’s actually the direction of the dependency: if the port belonged to whoever implements it, the centre would have to know the edge, and the inversion the whole pattern rests on wouldn’t be there.&lt;/p&gt;
&lt;p&gt;Where the adapter lives, on the other hand, depends on which boundary it crosses. In the normal case it sits in the infrastructure of the same module, wired by that module: ordinary clean architecture, entirely internal. But when an adapter would cross a boundary between modules it can’t sit there, because to implement that port it would have to import the other module — and that import is exactly what the boundary forbids. So the port stays with the consumer, and the adapter is provided and wired by the &lt;strong&gt;composition root&lt;/strong&gt;: the application that composes the modules.&lt;/p&gt;
&lt;p&gt;This was the thing I understood worst at the beginning, and I mistook it for a loophole — a place where the rules count for less. It’s the opposite. The composition root is the only point in the system with the right to know every piece, and it exists precisely so that everything else can avoid knowing each other: a module declares &lt;em&gt;what&lt;/em&gt; it needs without knowing &lt;em&gt;who&lt;/em&gt; will give it, and therefore stays compilable, testable and shippable on its own. Different deployment shapes become different roots composing the same modules with different adapters wired in, without a line inside the modules changing.&lt;/p&gt;
&lt;img alt=&quot;A port that crosses a module boundary: module A declares the port in its own application layer, and the composition root supplies and wires the adapter that implements it against module B, so neither module ever imports the other&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/architecture-as-a-standard-1.svg&quot; width=&quot;560&quot; height=&quot;610&quot; /&gt;
&lt;p&gt;&lt;em&gt;The port stays with the consumer that needs it. Only the root is allowed to know both
modules, which is what lets each of them compile and ship on its own.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;who-enforces-the-dependency-rule&quot;&gt;Who enforces the dependency rule?&lt;/h2&gt;
&lt;p&gt;So far this is a drawing, and a drawing doesn’t hold on its own. The difference between a real architecture and a diagram in a README is whether something fails when the rule is broken.&lt;/p&gt;
&lt;p&gt;The import graph is checked on every change by a dedicated tool — &lt;a href=&quot;https://github.com/sverweij/dependency-cruiser&quot;&gt;&lt;code&gt;dependency-cruiser&lt;/code&gt;&lt;/a&gt;, in my case — which is the single source of truth about boundaries and fails the build in CI. An inner layer importing an outer one, an inbound adapter importing an outbound one, a cycle between modules: all blocking errors, not observations left to a code review. And the constraint is declared in one place, instead of scattered across conventions each person remembers their own way.&lt;/p&gt;
&lt;p&gt;Two reinforcements matter as much as the main rule. The first is strict dependency resolution: a package a module hasn’t declared doesn’t even resolve, so the wrong import dies at build time and never reaches the linter. The second is that every module must be able to compile &lt;strong&gt;on its own&lt;/strong&gt;: it’s the most honest test of independence, because a hidden dependency doesn’t survive compiling in isolation — there’s no way to “nearly” pass it.&lt;/p&gt;
&lt;p&gt;This is the part that changes character when the other side is a model rather than a person. You explain a convention to a colleague and count on them remembering it, and if they forget you find out in review. An agent needs something that tells it no mechanically, immediately, every time — and that says it before hundreds of plausible lines have been written in the wrong place. A rule that lives only in a document is, to an agent, a suggestion.&lt;/p&gt;
&lt;h2 id=&quot;the-conventions-inside-the-layers&quot;&gt;The conventions inside the layers&lt;/h2&gt;
&lt;p&gt;Boundaries say where a thing may live; they don’t say how it’s written. That part is conventions, and their usefulness isn’t being clever — it’s being &lt;strong&gt;already decided&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The first is the role suffix in the filename: &lt;code&gt;*.use-case.ts&lt;/code&gt;, &lt;code&gt;*.port.ts&lt;/code&gt;, &lt;code&gt;*.adapter.ts&lt;/code&gt;, &lt;code&gt;*.repository.ts&lt;/code&gt;, &lt;code&gt;*.vo.ts&lt;/code&gt;, &lt;code&gt;*.controller.ts&lt;/code&gt;, &lt;code&gt;*.dto.ts&lt;/code&gt;. The role — and therefore the layer that file belongs in — reads at a glance, and that isn’t only a human benefit: it makes it immediately obvious, to an automated reviewer too, that a file named &lt;code&gt;*.repository.ts&lt;/code&gt; sitting in the application layer is in the wrong place.&lt;/p&gt;
&lt;p&gt;Then there’s how domain objects are constructed, which is the convention I care about most. A value object is immutable and validates on construction, through a static factory with a private constructor: from outside you cannot call &lt;code&gt;new&lt;/code&gt;, and the only available entrance validates and normalizes. The consequence is stronger than a recommendation — invalid state isn’t discouraged, it’s &lt;strong&gt;unrepresentable&lt;/strong&gt;: a malformed email never becomes a value object, so there is no point in the system it could reach. Entities follow the same logic with two distinct factories, one to create a new instance, which emits a domain event, and one to rebuild it from the database, which doesn’t — because re-reading a row isn’t something that just happened.&lt;/p&gt;
&lt;p&gt;Above the domain, the use cases: one per operation, one file each, flat, and &lt;strong&gt;zero business logic&lt;/strong&gt;. They orchestrate — call ports, build entities, use value objects — but the business rule lives in the domain, not there. Controllers are the mirror image: validate the DTO, map to a command, call the use case, map the response. No logic, ever.&lt;/p&gt;
&lt;p&gt;One practical detail worth stating because it’s specific to this stack: NestJS can’t inject a TypeScript interface, which doesn’t exist at runtime. So every port is an interface plus a &lt;code&gt;Symbol&lt;/code&gt; acting as its injection token. It’s the one place where the pattern has to bend to the language, and I’d rather say so than pretend the abstraction is free.&lt;/p&gt;
&lt;h2 id=&quot;two-layer-validation-and-the-error-model&quot;&gt;Two-layer validation, and the error model&lt;/h2&gt;
&lt;p&gt;There are two validations and they don’t duplicate each other, because they answer different questions. At the edge, on the DTO, you validate &lt;strong&gt;shape&lt;/strong&gt;: types, required fields, formats, body size. A malformed input is rejected immediately, before touching any logic, and it’s a security surface too. In the domain you validate &lt;strong&gt;business&lt;/strong&gt;, on construction, so that an invalid domain object doesn’t exist. The reason the domain doesn’t trust the DTO is concrete: not every entry point comes through a DTO. A CLI command, an event handler, a test build domain objects directly — if the business rules lived in the DTO validator, each of those roads would be an open door.&lt;/p&gt;
&lt;p&gt;Errors follow the same separation. There are two hierarchies, both framework-independent: one for business rule violations, one for technical faults. Persistence adapters wrap driver errors in the second, so a database error never reaches the user raw. And nothing is caught in the core: domain, use cases and controllers let it propagate. Exactly two points catch — the persistence adapter, translating the technical inward, and the inbound adapter, translating outward. Two points of translation instead of one scattered through every function.&lt;/p&gt;
&lt;p&gt;How it renders outward is by naming convention, not a hand-written map: an exception whose name ends in &lt;code&gt;NotFoundException&lt;/code&gt; becomes a 404, &lt;code&gt;AlreadyExistsException&lt;/code&gt; or &lt;code&gt;ConstraintException&lt;/code&gt; a 409, &lt;code&gt;PermissionsException&lt;/code&gt; a 403, any other domain violation a 422, and any technical fault a generic 500. The core stays entirely unaware of the protocol — which genuinely matters, because the same set of exceptions has to render on a channel that isn’t HTTP: from the CLI those same exceptions become exit codes and messages on stderr, not a mirror of HTTP statuses.&lt;/p&gt;
&lt;p&gt;One detail I learned the hard way and consider non-negotiable: every error carries a &lt;strong&gt;stable &lt;code&gt;code&lt;/code&gt;, decoupled from the class name&lt;/strong&gt;. If the code exposed to consumers were the class name, renaming a class — an internal refactor, invisible from outside — would break whoever relies on that code. The class name classifies the status; the &lt;code&gt;code&lt;/code&gt; is public contract, and the two need keeping apart.&lt;/p&gt;
&lt;h2 id=&quot;persistence-a-typed-query-builder-no-orm&quot;&gt;Persistence: a typed query builder, no ORM&lt;/h2&gt;
&lt;p&gt;For persistence I moved to a typed query builder — Kysely — and dropped the ORM. The reason isn’t stylistic, and it follows entirely from the pattern described so far.&lt;/p&gt;
&lt;p&gt;Aggregates, in this model, are &lt;strong&gt;detached&lt;/strong&gt;: the repository reads rows and translates them with a mapper, and the domain object that comes out isn’t “managed” by anything. Between aggregates there are no relations and no foreign keys — integrity is application-level and cascades are orchestrated by the use cases. Which means every convenience an ORM offers is inapplicable: nothing to track, nothing to lazy-load, nothing to synchronize. And an inapplicable but available convenience isn’t neutral: it’s implicit state sitting there for whoever uses it by mistake, with effects that surface far from where it was used. On a managed object model an ORM is the right call; here it would be an engine left running under the floor.&lt;/p&gt;
&lt;h3 id=&quot;the-migration-is-the-only-declaration-of-the-schema&quot;&gt;The migration is the only declaration of the schema&lt;/h3&gt;
&lt;p&gt;The consequence that convinced me most, though, is about declaring the schema. &lt;strong&gt;The migration is the only declaration of the schema&lt;/strong&gt;: the types describe the &lt;em&gt;shape&lt;/em&gt; of the rows — column names, types, what can be omitted on insert — while the &lt;em&gt;structures&lt;/em&gt;, meaning primary keys, unique constraints and indexes, live only in the migration that creates them. From which follows something worth stating in full: there is no schema-versus-types drift check, because there is no second declaration to reconcile. An ORM requires that check &lt;em&gt;because&lt;/em&gt; it forces you to declare twice — the entity metadata and the migration SQL — so the check solves a problem the tool itself introduced. Here that class of error isn’t caught better: it isn’t representable.&lt;/p&gt;
&lt;p&gt;I have to state the reverse too, because it appears to contradict what I just wrote. Columns — name, type, nullability — are described twice, in the migration and in the row type, and nothing verifies it mechanically. That’s a choice, not an oversight: a mismatch between types and schema isn’t a catastrophic or irreversible invariant, so it doesn’t warrant a blocking check, and the burden stays with whoever writes it. In practice the immediate effects police it: column names go straight into the SQL, so a wrong one fails the integration tests against a real Postgres right away; a column declared and absent breaks the query selecting it. What’s genuinely left uncovered is nullability on a path no test exercises — and I’d rather know that and say it than believe I’m covered.&lt;/p&gt;
&lt;p&gt;One last detail that strikes me as the most elegant part of all this: each migration is typed against the world it was born in, not the current schema. Otherwise code that is immutable by definition — an applied migration is never touched — would depend on something that evolves, and an old migration would stop compiling at the first schema change.&lt;/p&gt;
&lt;h2 id=&quot;why-this-suits-working-with-an-ai&quot;&gt;Why this suits working with an AI&lt;/h2&gt;
&lt;p&gt;The benefit is narrower than the topic suggests, and I want to calibrate it precisely: an explicit standard plus a check that actually runs does not make generated code &lt;strong&gt;correct&lt;/strong&gt;. It makes it &lt;strong&gt;placeable&lt;/strong&gt;, and it makes boundary errors visible early and mechanically. Those are two different things, and only the second is what architecture buys you.&lt;/p&gt;
&lt;p&gt;Placeable means the question “where does this piece go” already has an answer before the model starts. It doesn’t have to invent a structure, and therefore can’t invent a different one next week for a similar feature: the arbitrary part — the thing I said at the start is the real point of failure — has been removed from the problem, not delegated more skilfully. And there’s a side effect I appreciated more than I expected: the architecture doesn’t have to live in the prompt. It lives in the repository, in normative files the model reads, and it doesn’t need repeating every conversation or remembering by me.&lt;/p&gt;
&lt;h3 id=&quot;splitting-the-verification-work&quot;&gt;Splitting the verification work&lt;/h3&gt;
&lt;p&gt;Then there’s a division of verification labour that turned out to be the most useful part. &lt;strong&gt;Structural&lt;/strong&gt; violations — a layer importing outward, an inbound adapter touching an outbound one, a module that won’t compile alone — aren’t matters of judgement: a machine catches those, always, and they block. What’s left over are the shades of intent: whether that’s really orchestration or business logic in disguise, whether the port is declared on the right side. Those are judgements, and they can be made by a reviewer — human or model — reading &lt;strong&gt;the standard itself&lt;/strong&gt; as its rubric, not a checklist derived from it. That detail matters: a checklist summarized from a document drifts away from the document; if the reviewer reads the document, there’s nothing that can diverge.&lt;/p&gt;
&lt;p&gt;The final accounting is this, and it returns to where I started. The cost had moved to checking, and checking mountains of code I didn’t write is the expensive part. I haven’t eliminated it. I’ve made one entire class of it — boundaries, which is precisely where these tools go wrong — either impossible or immediately obvious, so that the attention I have left can go to what a machine can’t judge.&lt;/p&gt;
&lt;h2 id=&quot;what-it-deliberately-leaves-open&quot;&gt;What it deliberately leaves open&lt;/h2&gt;
&lt;p&gt;It’s worth saying where this standard is silent, because the silence is designed as much as the rules.&lt;/p&gt;
&lt;p&gt;It fixes &lt;strong&gt;structure&lt;/strong&gt;, not style. It says where a piece of code goes and who may know whom; it doesn’t say how you write inside a layer, how to decompose an algorithm, how to name a local variable, which library to pick for a contained job. Inside a use case or inside an adapter there’s all the freedom there ever was. That’s a choice about proportion, and it matches this article’s thesis: rigidity should be spent where these tools fail, on the boundaries, and not spent where they’re already reliable. Constraining implementation too would cost review attention and buy nothing — and a standard that constrains everything is a standard nobody keeps for long.&lt;/p&gt;
&lt;h2 id=&quot;what-actually-moved&quot;&gt;What actually moved&lt;/h2&gt;
&lt;p&gt;The value that seemed to be depreciating wasn’t mine: it was one particular form I used to put it in. Writing code was the visible part of the job, but it wasn’t the part the expertise lived in — and now that the visible part costs almost nothing, what’s left making the difference is knowing where the lines go, and knowing it precisely enough to write it as a rule a machine can check.&lt;/p&gt;
&lt;p&gt;That’s the paradox of this period, for me: a tool that writes code in my place has made me a more theoretical engineer than I was. I had to properly study what I had only ever practised, because to demand that a model respect a boundary, I have to be able to define it myself first.&lt;/p&gt;
</content:encoded><category>architecture</category><category>ai</category></item><item><title>An air conditioner on my own network, and nobody else&apos;s</title><link>https://adrianofoschi.com/blog/air-conditioner-without-a-cloud/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/air-conditioner-without-a-cloud/</guid><description>The boxes you can buy to control an air conditioner remotely route the command through the manufacturer&apos;s data centre. I soldered my own instead: an ESP8266, an infrared LED and a receiver, running Tasmota — and it ended up inside the machine it controls.</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The air conditioner at home is controlled by its remote, and by nothing else. It has no network socket, it talks to nothing, and if the remote is in the other room you get up.&lt;/p&gt;
&lt;p&gt;The little boxes you can buy to solve this have been around for years: they fire infrared on your behalf and you drive them from an app. The thing is, that app talks to the manufacturer’s server, so the command to switch on an appliance three metres away takes a trip through a data centre. If that service shuts down, or changes its terms, or is simply out, the air conditioner goes back to being a machine with a remote.&lt;/p&gt;
&lt;p&gt;I wanted all of it to stay inside the house. The goal was to get the air conditioner into &lt;a href=&quot;https://www.home-assistant.io/&quot;&gt;Home Assistant&lt;/a&gt; — the home automation system that holds a household’s devices together, running on a machine of mine rather than somebody else’s — so I could switch it on, switch it off and, above all, automate it along with everything else. In 2020 I built the thing myself.&lt;/p&gt;
&lt;h2 id=&quot;its-a-blinking-led-really&quot;&gt;It’s a blinking LED, really&lt;/h2&gt;
&lt;p&gt;There is nothing mysterious inside an infrared remote. It’s a diode that emits light beyond the range of the eye, switching it on and off very fast in a pattern, and the air conditioner has a receiver that knows how to read that pattern. Point a phone camera at one and you can see it flicker, because plenty of sensors don’t filter it out.&lt;/p&gt;
&lt;p&gt;So not much is needed: something that sits on the home network, an infrared LED, and a component to drive it — a microcontroller pin can’t supply the current an LED needs to be seen a few metres away. I used a transistor, with a resistor to limit the current.&lt;/p&gt;
&lt;p&gt;Then there’s the part few people think about: a &lt;strong&gt;receiver&lt;/strong&gt;. A transmitter on its own works, but it works blind. It fires the command and doesn’t know whether it arrived; more to the point, it knows nothing about what happens when somebody picks up the real remote and changes the temperature. With a receiver alongside it, the device also listens to the infrared going past, and what happens in the room stops being invisible.&lt;/p&gt;
&lt;h2 id=&quot;from-breadboard-to-perfboard&quot;&gt;From breadboard to perfboard&lt;/h2&gt;
&lt;p&gt;The first step was on a breadboard, with jumper wires: the board, the LED, the transistor, the resistor. The simplest test there is — aim the LED at the split unit and see whether it responds.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A breadboard on a desk holding a clear infrared LED, a small black transistor and a resistor, wired with orange and blue jumper leads to an ESP8266 development board lying beside it&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1600&quot; height=&quot;1200&quot; src=&quot;https://adrianofoschi.com/_astro/breadboard.oyFNQMZq_Z2jN6VF.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The first version, on a breadboard. Enough to find out whether the air conditioner would answer at all.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Then the same thing soldered onto a scrap of perfboard the size of a stamp, transmitter and receiver on the same little board, with the LED left on its long legs so it can be pointed where it’s needed. The board is one of those carrying an ESP8266: the few-euro wifi module that made putting anything at all on a network normal.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The finished assembly laid out on a table: a blue mains-to-5V module, the small soldered board with the infrared LED standing off it, a temperature sensor on a lead, a mains plug, and the ESP8266 board, all wired together&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1200&quot; height=&quot;1601&quot; src=&quot;https://adrianofoschi.com/_astro/soldered.Ly4KlR8Q_23Gjrl.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Everything wired together before going into the box: the mains module, the infrared board, the sensor on its lead, and the microcontroller.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It isn’t precision work. It’s an afternoon with a soldering iron, and it looks like it.&lt;/p&gt;
&lt;h2 id=&quot;power-decides-the-shape-of-the-object&quot;&gt;Power decides the shape of the object&lt;/h2&gt;
&lt;p&gt;At this point the question stops being electronics and becomes shape: how does this thing sit in a room.&lt;/p&gt;
&lt;p&gt;A USB power supply was the short road, and it would have produced the object you see everywhere — a little box with a cable running down to a socket. Instead I put a module inside the box that takes mains voltage and gives back the 5 volts the board needs: two fingers wide, a few euros.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The inside of a small white plastic enclosure: an ESP8266 development board fills most of it, with the blue mains-to-5V module standing alongside and coloured wires tucked underneath&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1200&quot; height=&quot;1601&quot; src=&quot;https://adrianofoschi.com/_astro/enclosure.CCm7PoKX_Z1Bcvz7.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Inside the box: the board, and next to it the module that brings the mains down to 5 volts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That choice does mean mains voltage runs inside the enclosure, and the care that calls for is of a different order: insulate properly, leave nothing exposed, close it up. Not a detail to wave through.&lt;/p&gt;
&lt;p&gt;The advantage isn’t the price, it’s that the object becomes self-contained: a closed box with two wires going in and nothing around it. From there you can put it anywhere there’s power — which is exactly what made the final arrangement possible.&lt;/p&gt;
&lt;h2 id=&quot;the-sensor-the-air-conditioner-doesnt-have&quot;&gt;The sensor the air conditioner doesn’t have&lt;/h2&gt;
&lt;p&gt;On the same board I added a temperature and humidity sensor, on a lead long enough to place it where it’s useful. It wasn’t in the original plan, but once you have a networked device attached to the air conditioner, not putting a sensor on it is a waste.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The components spread out: the mains-to-5V module labelled 100-240VAC to 5VDC, the ESP8266 board in the middle, the small soldered board with its infrared LED, and a white temperature and humidity sensor on a long twisted lead at the right&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1600&quot; height=&quot;1200&quot; src=&quot;https://adrianofoschi.com/_astro/components.43N1Po9C_2kxsF8.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The sensor is the white grille on the right, deliberately on a long lead so it wouldn’t have to sit wherever the rest of the electronics ended up.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There’s a practical reason too. The air conditioner measures temperature with its own probe, which sits inside the unit, high up near the ceiling, where the air is warmer than the air people are actually in. That number is fine for the machine’s own thermostat, but if you want to trigger something based on how warm &lt;em&gt;the room&lt;/em&gt; is, you need a reading taken somewhere else.&lt;/p&gt;
&lt;p&gt;Then the installation got in the way. With the box ending up inside the air conditioner, a sensor in there measures the machine’s air, not the room’s. It sits inside, but as far from the motor as it can be, almost looking out. It’s a compromise, and it may as well be said plainly: it isn’t the same as a sensor standing free in the middle of the room, but it’s a far closer reading of the room than the one the split gives.&lt;/p&gt;
&lt;h2 id=&quot;tasmota-replacing-the-firmware-instead-of-writing-one&quot;&gt;Tasmota: replacing the firmware instead of writing one&lt;/h2&gt;
&lt;p&gt;I didn’t write any firmware for the microcontroller. I put &lt;a href=&quot;https://tasmota.github.io/docs/&quot;&gt;Tasmota&lt;/a&gt; on it — free firmware born to free smart plugs from the manufacturer’s cloud. You install it in place of the factory one, and the device starts speaking MQTT on the home network, with a configuration page and nothing else around it.&lt;/p&gt;
&lt;p&gt;It goes on over serial, with a USB adapter wired to the board’s pins. First the microcontroller has to be put into programming mode, GPIO0 pulled to ground as the power comes up, and then the memory is written from the command line: erase it, then write the firmware at address zero.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;esptool.py --port /dev/ttyUSB0 erase_flash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;esptool.py --port /dev/ttyUSB0 write_flash -fm dout 0x0 tasmota-ir.bin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filename matters: &lt;strong&gt;tasmota-ir&lt;/strong&gt;, not plain Tasmota. A separate build exists because the library that speaks infrared carries an enormous number of protocols — every air conditioner manufacturer has its own — and all of it together doesn’t fit alongside the rest of the features. So the project made a build that keeps almost every IR protocol and gives other things up. With standard Tasmota you can’t drive the air conditioner at all.&lt;/p&gt;
&lt;p&gt;Then you tell the firmware which pin does what: one to send, one to receive, one for the sensor.&lt;/p&gt;
&lt;p&gt;Finding the right protocol was the long part. The library knows several LG variants, and nobody tells you which one your appliance speaks: you try one, watch whether the split responds, change it. It took me a good many attempts. In the end it was the one Tasmota calls &lt;code&gt;LG2&lt;/code&gt;. And this is where the receiver stopped being a luxury: press a button on the real remote, read what Tasmota made of it — protocol, mode, temperature — and hold that up against what you were about to send.&lt;/p&gt;
&lt;h2 id=&quot;where-it-ended-up&quot;&gt;Where it ended up&lt;/h2&gt;
&lt;p&gt;For a while the box stayed where it’s natural to put it: on the wall under the split, white on white, its cable running down to the socket.&lt;/p&gt;
&lt;p&gt;Then I took the last step. There is already power inside the air conditioner, and the module I’d put in the box starts from mains voltage anyway, so it only had to be connected there. The device ended up inside the casing of the indoor unit, powered by the machine it controls, with the LED aimed where it needs to be and the sensor kept as far from the motor as possible. From outside there’s nothing to see: there’s an air conditioner, exactly as before.&lt;/p&gt;
&lt;p&gt;It has been working since 2020. In those years the only thing I’ve had to go back to is the configuration on the Home Assistant side, which has changed a great deal in the meantime. The hardware, no — that just sits there.&lt;/p&gt;
&lt;p&gt;Five years is a long time for something soldered onto a scrap of perfboard in an afternoon. The reason it has held, I think, is that it depends on nothing: it has no account, it calls no server, and the only thing it has to do is light an LED at the right moment.&lt;/p&gt;
</content:encoded><category>homelab</category></item><item><title>Quality was nobody&apos;s job, and then it was mine</title><link>https://adrianofoschi.com/blog/quality-was-nobodys-job/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/quality-was-nobodys-job/</guid><description>Eight years at a software house, half in consulting and half building the company&apos;s own product. What decided which years I thrived in wasn&apos;t the contract label — it was that I&apos;ve never been someone who sits still.</description><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;From 2015 to 2023 I worked as a full-stack developer at a software house: consulting until 2019, on clients in banking, insurance and e-learning; the company’s own product, a B2B SaaS, from 2019 on. For years I told these eight years as consulting-versus-product. But what I actually learned isn’t about the contract label — it’s about me: I’m not someone who sits still on a system that’s already built. I like to learn, to build, to experiment. And it’s that instinct, not the kind of contract, that decided which of those years actually felt right.&lt;/p&gt;
&lt;h2 id=&quot;what-consulting-actually-is-being-the-vendor&quot;&gt;What consulting actually is: being the vendor&lt;/h2&gt;
&lt;p&gt;Consulting has a precise shape: you’re the vendor. You work on a system that isn’t yours, inside decisions made by other people before you arrived. You’re paid to deliver an agreed scope by an agreed date — that’s, literally, the definition of “done.” Almost all of it was work on software that already existed, at banks and insurers with their own architects and their own standards, settled years before I showed up. The work often came through subcontracting — a detail of my situation, not what mattered. You’re not brought in to have opinions; you’re brought in because there’s more work than there are people. Inside that edge the work was real: Scrum, environments you can’t touch directly, deployments with a window and a sign-off, documentation written for an auditor more than for a developer. I got good at it, and it’s a skill I still carry.&lt;/p&gt;
&lt;h2 id=&quot;why-quality-wasnt-a-requirement-and-nobody-said-so&quot;&gt;Why quality wasn’t a requirement, and nobody said so&lt;/h2&gt;
&lt;p&gt;What I remember most, though, is how little anything moved. Technology choices were effectively closed — not defended, closed: a thing was done a certain way because it had always been done that way, and proposing otherwise had nowhere to land. Those questions belonged to the client’s own architects, answered years earlier. You could flag a risk and watch it get filed. Not rejected. Filed. I don’t think that was bad luck: when the software is already there and belongs to someone else, resisting change is the default state, and low quality is what years of that default leave behind. And nobody talked about it — not cynicism, closer to a tacit agreement: if something shipped and nothing caught fire, it was fine. Quality wasn’t a requirement traded away against cost. It simply wasn’t a requirement.&lt;/p&gt;
&lt;h2 id=&quot;the-small-clients-where-i-owned-every-decision&quot;&gt;The small clients, where I owned every decision&lt;/h2&gt;
&lt;p&gt;Alongside the large engagements there was a steady stream of smaller projects, for clients who came once. On those I ran everything myself — from the first conversation to delivery, technical choices included, with the responsibility that comes attached. They were solutions built from zero, with no architecture to inherit and no one to ask permission from: an idea could be tried on Tuesday and be in production by Thursday. On paper it was consulting, same as the large accounts; in substance I was building a product for a client, from scratch — and that, not the contract label, is why I learned more there. On the large accounts, instead, the goal was delivering the agreed scope by the date, full stop. I understand the real pressures of a legacy system and a regulated release — but I wouldn’t use them as an excuse for what I saw: in an environment handling other people’s money and data, quality and security should matter more, not less. Instead, shipping without real care wasn’t a problem for whoever was in the room at the time: it would be a problem for whoever came after — often another vendor, not me. That comfort, not the system’s complexity, is the part I don’t buy.&lt;/p&gt;
&lt;h2 id=&quot;what-changed-when-the-product-became-ours&quot;&gt;What changed when the product became ours&lt;/h2&gt;
&lt;p&gt;Moving onto the company’s own product in 2019 changed three things, and none of them was the technology. First: technical decisions became available — not automatically won, but arguable: proposing a better approach turned into a normal part of the job instead of a category error. Second: consequences came back. Code I wrote badly, I met again six months later, and so did everyone around me — quality stops being a line item in someone else’s signed-off estimate and becomes something you personally pay for or benefit from. Third: the time horizon. A project ends. A product continues — refactoring, debt carried on purpose, migrations, maintenance with no final release. You start thinking in years, because the system will still be there in years, and so, probably, will you. And it’s exactly this terrain — where a proposal has somewhere to land, where you get it wrong and learn again, where you build something that lasts — that suits me: not because it was “product” instead of “consulting,” but because it’s where I could keep learning, building, experimenting.&lt;/p&gt;
&lt;h2 id=&quot;the-risk-on-the-product-side-losing-the-compass&quot;&gt;The risk on the product side: losing the compass&lt;/h2&gt;
&lt;p&gt;There’s a risk on this side too, and it’s honest to admit it: on an internal product the roadmap bends constantly — a customer asks, an opportunity opens up this quarter — and every single bend is defensible. What’s at risk is the medium and long term: the direction you set when you were thinking clearly, which nobody ever cancels, just keeps getting deferred one reasonable decision at a time. Losing the compass never feels like a decision, which is exactly what makes it hard to notice and even harder to argue against.&lt;/p&gt;
&lt;p&gt;From these eight years I took two different things. From the first half, discipline: scoping honestly, delivering what was agreed, reading an organization well enough to know what could actually change and what couldn’t. From the second, something harder to put on a CV: owning the consequences is the only mechanism that makes quality matter to anyone — but it needs the right environment to exist at all. If I’m honest, though, what pulled me toward product wasn’t quality in the abstract. It’s that I’m not someone who sits still. I like to learn, to build, to experiment, and product is where that instinct gets to run instead of being filed away with everything else.&lt;/p&gt;
&lt;p&gt;There’s a coda to this. Years later, the technical direction of that same product became my responsibility. The compass problem I’d watched from a developer’s seat turned into the thing I answer for. It’s a fair outcome, and an uncomfortable one: I can no longer file the observation. But it’s also, exactly, what I wanted — a place to build for long enough to have to answer for it.&lt;/p&gt;
</content:encoded><category>career</category></item><item><title>A wallet inside a chat app that holds no keys</title><link>https://adrianofoschi.com/blog/wallet-in-a-chat-app/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/wallet-in-a-chat-app/</guid><description>Putting a self-custody wallet in Telegram means putting it somewhere that must never be allowed to sign anything. The way out was to stop treating the bot as a wallet and start treating it as a remote control.</description><pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The hardest part of a wallet has never been the wallet. It’s that using one starts with installing something, and most people won’t.&lt;/p&gt;
&lt;p&gt;Chat apps are the obvious way out. People are already in them all day, a bot costs nothing to try, and sending money to someone you’re in the middle of a conversation with is a natural act rather than an errand to run. So I built a Telegram front end for Sovrano, the smart-account wallet I’m building on &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And immediately ran into the thing that makes chat wallets quietly awful.&lt;/p&gt;
&lt;h2 id=&quot;a-bot-is-a-server&quot;&gt;A bot is a server&lt;/h2&gt;
&lt;p&gt;Telegram bots are not code running on your phone. A bot is a process on somebody else’s machine, receiving your messages through an API. If that process can sign a transaction then it holds a key — and a service holding your key on your behalf is a custodian, which is precisely the arrangement self-custody exists to avoid.&lt;/p&gt;
&lt;p&gt;Most wallets inside chat apps resolve this by being custodial and not dwelling on the point. The money is theirs until you withdraw it. It works, it’s convenient, and it’s a different product wearing the same word.&lt;/p&gt;
&lt;p&gt;I wanted the other thing: the reach of a chat interface, without the bot ever being able to move a coin. Which meant accepting up front that the bot could not be a wallet. It could only be the remote control for one.&lt;/p&gt;
&lt;h2 id=&quot;every-signature-leaves-the-chat&quot;&gt;Every signature leaves the chat&lt;/h2&gt;
&lt;p&gt;So the bot holds nothing. It knows which addresses you’re watching, because it has to show you balances, and that’s where it ends. Logging in means typing your nickname: it isn’t authentication, it’s telling the interface what to look at.&lt;/p&gt;
&lt;p&gt;Whenever something genuinely needs a signature, the flow goes elsewhere. The bot prepares the operation — recipient, token, amount — and replies with a link. You approve it in the browser, with whichever authentication method you chose for your account: a passkey and your fingerprint, or a login with Google, Facebook, Discord, or Telegram itself. The signed result comes back to the bot, which drops you back into the conversation, and that link burns there: it’s good once.&lt;/p&gt;
&lt;p&gt;The bot never sees a key, at any point. It sees an intention going out and a signed result coming back.&lt;/p&gt;
&lt;p&gt;That the signature has to happen outside the chat isn’t an inconvenience to engineer around, incidentally — it’s the security property working. Whatever method you pick, the credential isn’t in the bot and can’t be reached from there: a passkey is bound to the site that created it and needs the operating system’s own prompt, a social login forces you through the provider. There is deliberately no way for a bot to trigger either one on your behalf, and that is exactly why a compromised bot can’t quietly authorize anything.&lt;/p&gt;
&lt;p&gt;There’s a case that makes it obvious: you can sign with Telegram itself, with the same identity you’re using to talk to the bot. The authorization still leaves the chat, and the bot still stays outside it.&lt;/p&gt;
&lt;img alt=&quot;A payment through the bot: the bot prepares the operation and hands back a single-use link, the signature happens on an approval page outside the chat using a passkey or a social login, and only the signed result returns to the bot&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/wallet-in-a-chat-app-0.svg&quot; width=&quot;833&quot; height=&quot;864&quot; /&gt;
&lt;p&gt;&lt;em&gt;The bot sees an intention going out and a signed result coming back. At no point in the
sequence is there a step where it holds a key.&lt;/em&gt;&lt;/p&gt;
&lt;video controls preload=&quot;metadata&quot; poster=&quot;https://adrianofoschi.com/videos/blog/wallet-in-a-chat-app-poster.jpg&quot; aria-label=&quot;The actual flow in Telegram: /start in the bot, an auth.sovrano.app page opening to register a nickname and a passkey, choosing a sign-in method, and the bot confirming the account was added&quot; style=&quot;width:100%;max-width:320px;margin:0 auto;display:block;aspect-ratio:9/20;border:1px solid var(--color-border);border-radius:0.375rem&quot;&gt;
	&lt;source src=&quot;https://adrianofoschi.com/videos/blog/wallet-in-a-chat-app.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;p&gt;&lt;em&gt;The same handoff, recorded: &lt;code&gt;/start&lt;/code&gt;, the bot hands off to &lt;code&gt;auth.sovrano.app&lt;/code&gt;, a passkey is registered, and the bot confirms the account without ever seeing it.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;chat-turns-out-to-be-a-good-interface&quot;&gt;Chat turns out to be a good interface&lt;/h2&gt;
&lt;p&gt;The rest was more fun than I expected. Bots are bad at dashboards and unusually good at asking one question at a time — which happens to be the right shape for sending money.&lt;/p&gt;
&lt;p&gt;Which token: here are your balances, as buttons. To whom: type a nickname, and it resolves. How much. Here’s the summary, confirm. That’s a wizard, and a wizard inside a chat doesn’t feel like a form to fill in: it feels like the conversation you were already having. Balances, transaction history, NFTs and deposit addresses are all just messages with buttons underneath.&lt;/p&gt;
&lt;p&gt;For now the bot stops at those features. I intend to add more, even if it won’t be simple, within the limits of what Telegram’s interface allows.&lt;/p&gt;
&lt;h2 id=&quot;where-an-interface-can-live&quot;&gt;Where an interface can live&lt;/h2&gt;
&lt;p&gt;The bot is proof of something more general. Almost everything a wallet interface does — finding the right token, resolving a nickname, formatting an amount, showing you what you’re about to confirm — has no need to touch a secret. Only the signature does. And the bot doesn’t produce that signature: it delegates it to an external gateway that opens in the browser, the same piece any other application can call to have an operation authorized.&lt;/p&gt;
&lt;p&gt;The real point is that not even that gateway holds your keys. Whether the account is controlled by a passkey, by an OAuth2 identity or by the usual twelve words, the credential stays where it is — on your device, with your provider, in your head — and the gateway only asks. Which is why the interface can live anywhere at all: inside a chat, inside a website, inside any place you’d never trust with a key, because there is no key there.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>Sovrano: a self-custody wallet you open with a fingerprint</title><link>https://adrianofoschi.com/blog/sovrano-wallet/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/sovrano-wallet/</guid><description>A wallet where creating a blockchain account means touching a fingerprint sensor or signing in with Google — no seed phrase, no extension, no tokens to buy first. Built on modular smart accounts, so the rules live in the account itself.</description><pubDate>Tue, 17 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Self-custody is presented as freedom, and it is, but the fine print is that you become your own bank’s entire security department. Write down twelve words. Store them somewhere a fire won’t reach and a guest won’t find. Never type them into anything. Lose them and your money is gone, with no one to appeal to.&lt;/p&gt;
&lt;p&gt;Then, before you can do anything at all, install a browser extension, and acquire some of the network’s token to pay fees with — using, presumably, the wallet you don’t have yet.&lt;/p&gt;
&lt;p&gt;I built Sovrano to find out how much of that could simply be deleted.&lt;/p&gt;
&lt;h2 id=&quot;what-sovrano-is-a-wallet-you-open-with-a-fingerprint&quot;&gt;What Sovrano is: a wallet you open with a fingerprint&lt;/h2&gt;
&lt;p&gt;Sovrano is a self-custody wallet on &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt;, a blockchain with no transaction fees. It’s built on &lt;a href=&quot;https://github.com/veive-io&quot;&gt;Veive&lt;/a&gt;, a modular smart-account protocol I’d written for the same chain, where an account isn’t a key pair but a contract, and its behaviour — how a signature is checked, what an operation is allowed to do — comes from modules installed into it.&lt;/p&gt;
&lt;p&gt;That’s the load-bearing choice. If the account is a contract that decides for itself what counts as a valid signature, then “sign in with your fingerprint” isn’t a convenience layer bolted onto a key. It’s the account’s actual authentication rule, enforced on chain.&lt;/p&gt;
&lt;p&gt;And because the account lives on the chain rather than on a device, it isn’t tied to any platform. Sovrano runs as a PWA in any browser — desktop or mobile, on any operating system — and as a native Android app. There’s no key file to move from one device to another: you register a passkey on each device you want to use, and each one reaches the same account.&lt;/p&gt;
&lt;p&gt;So sign-up looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Sovrano sign-up screen: a prominent “Passkey” button described as biometric or hardware-key authentication, a row of Google, Apple, Microsoft, X and Facebook buttons under “or continue with”, and a footnote reading “You can still use mnemonic but it is not recommended”&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;420&quot; height=&quot;707&quot; src=&quot;https://adrianofoschi.com/_astro/signup-passkey.DOD1tGu6_1UQ7hX.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Sign-up. Note the last line: the seed phrase survives as a discouraged fallback, not the default.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A passkey, an account you already have, or — if you insist — twelve words. Inverting that default was the entire point. No extension to install, no tokens to acquire before you start, and nothing to write on paper.&lt;/p&gt;
&lt;video controls preload=&quot;metadata&quot; poster=&quot;https://adrianofoschi.com/videos/blog/sovrano-signup-x-poster.jpg&quot; aria-label=&quot;Signing up to Sovrano Wallet using an X (Twitter) account&quot; style=&quot;width:100%;aspect-ratio:16/9;border:1px solid var(--color-border);border-radius:0.375rem&quot;&gt;
	&lt;source src=&quot;https://adrianofoschi.com/videos/blog/sovrano-signup-x.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;p&gt;&lt;em&gt;Signing up with an account you already have: pick a nickname, authorize with X, done.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;passkeys-and-hardware-keys&quot;&gt;Passkeys, and hardware keys&lt;/h2&gt;
&lt;p&gt;Passkeys were the straightforward half: &lt;a href=&quot;https://www.w3.org/TR/webauthn-2/&quot;&gt;WebAuthn&lt;/a&gt; produces a signature, the account contract verifies it, done.&lt;/p&gt;
&lt;p&gt;What I like most about that approach is what it opens up as a consequence. A passkey doesn’t have to live in a phone: WebAuthn also talks to FIDO2 security keys — a YubiKey, a SoloKey, or any cheap USB key. Plug it in, enter the PIN, touch it to confirm, and you have the security of a hardware wallet without buying one: the keys never leave the device, physical presence is required to approve a transaction, and there’s no seed phrase in existence to be stolen or intercepted.&lt;/p&gt;
&lt;video controls preload=&quot;metadata&quot; poster=&quot;https://adrianofoschi.com/videos/blog/sovrano-hardware-passkey-poster.jpg&quot; aria-label=&quot;Signing in to Sovrano Wallet using a hardware passkey&quot; style=&quot;width:100%;aspect-ratio:16/9;border:1px solid var(--color-border);border-radius:0.375rem&quot;&gt;
	&lt;source src=&quot;https://adrianofoschi.com/videos/blog/sovrano-hardware-passkey.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;p&gt;&lt;em&gt;Signing in with a hardware passkey: the PIN, then a physical touch on the key.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;making-sign-in-with-discord-into-a-signature&quot;&gt;Making “sign in with Discord” into a signature&lt;/h2&gt;
&lt;p&gt;Social login was the awkward half. &lt;a href=&quot;https://openid.net/developers/how-connect-works/&quot;&gt;OpenID Connect&lt;/a&gt; providers issue a signed ID token, and a contract can verify that token’s signature and read its claims — that part works. But plenty of the accounts people actually have aren’t OpenID providers. X, Discord and Telegram authenticate you perfectly well and then hand you something that is not an ID token.&lt;/p&gt;
&lt;p&gt;So I wrote an identity broker: a small &lt;a href=&quot;https://www.passportjs.org/&quot;&gt;Passport&lt;/a&gt; service that speaks each provider’s own dialect — OAuth for Google, whatever Discord and X and Telegram want — normalizes the result into a subject like &lt;code&gt;google|1234&lt;/code&gt;, and mints an RS256-signed ID token of its own, published with a JWK so anything can verify it. Downstream, every provider looks identical: one token shape, one signature algorithm, one public key. The on-chain module doesn’t need to know which one you used.&lt;/p&gt;
&lt;video controls preload=&quot;metadata&quot; poster=&quot;https://adrianofoschi.com/videos/blog/sovrano-social-login-poster.jpg&quot; aria-label=&quot;Signing up with any of several identity providers, then approving an operation through the same screen regardless of which one was used&quot; style=&quot;width:100%;aspect-ratio:16/9;border:1px solid var(--color-border);border-radius:0.375rem&quot;&gt;
	&lt;source src=&quot;https://adrianofoschi.com/videos/blog/sovrano-social-login.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;p&gt;&lt;em&gt;Google, Apple, Microsoft, X, Facebook: whichever one you picked, the broker is what makes the result verifiable on chain.&lt;/em&gt;&lt;/p&gt;
&lt;img alt=&quot;The identity broker: providers that are OpenID and providers that are not both arrive at one small service, which normalizes the subject and mints a single RS256 ID token, so the on-chain module only ever verifies one token shape against one public key&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/sovrano-wallet-0.svg&quot; width=&quot;558&quot; height=&quot;525&quot; /&gt;
&lt;p&gt;&lt;em&gt;Downstream of the broker every provider looks identical, which is what the contract needs.
The cost is that the broker signs those tokens, so it can mint an identity for anyone.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I want to be straight about the cost of that. The broker signs those tokens, so the broker is a trusted issuer, and whoever holds its private key can mint an identity for anyone. In a wallet whose whole premise is self-custody, that’s a real centralization point sitting next to the front door. The honest framing is that it was a deliberate trade — reach now, decentralize the issuer later — and “later” is doing a lot of work in that sentence.&lt;/p&gt;
&lt;h2 id=&quot;what-you-install-into-the-account&quot;&gt;What you install into the account&lt;/h2&gt;
&lt;p&gt;Removing the barriers to entry was half the work. The other half is what that account can do once you have it — and this is where building on modular smart accounts stops being an architectural detail and becomes the product.&lt;/p&gt;
&lt;p&gt;Because the account is a contract and its behaviour comes from installed modules, the wallet isn’t a fixed set of features: it’s a container you add rules to. Some of those modules are what the wallet itself stands on: signature validation, the authentication modules for passkeys and OpenID, and allowance — the pre-authorization that records exactly the approved operation and consumes it on use, so an application never holds a broader permission than the one you gave it.&lt;/p&gt;
&lt;p&gt;Then there’s multisig, and that’s where a personal wallet becomes something else. The same account can be managed by several people, with transactions requiring signatures from a configurable number of authorized members: a business treasury, a DAO, an investment group. And the same mechanism, applied to the module install and uninstall scopes, becomes recovery: you appoint trusted guardians, register several devices and several social accounts, and if you lose access to one of them the others can authorize replacing it. Account recovery without a seed phrase existing to be recovered.&lt;/p&gt;
&lt;p&gt;Beyond those, the same module structure makes writable things a wallet normally can’t offer, because they don’t live in the wallet — they live in the account: a piggybank that sets aside a percentage of every payment, a daily or weekly spending limit, parental control over a child’s account, a subscription guard that catches recurring payments. I didn’t write those — they’re the use cases I documented as a demonstration of what the system allows, and they remain the most interesting part of what there was still to build.&lt;/p&gt;
&lt;h2 id=&quot;two-parts-the-wallet-and-the-payment-gateway&quot;&gt;Two parts: the wallet and the payment gateway&lt;/h2&gt;
&lt;p&gt;It’s worth being clear about the structure here, because Sovrano isn’t one product but two. There’s the wallet, and there’s the payment gateway.&lt;/p&gt;
&lt;p&gt;The gateway runs in the browser, and it works the way PayPal or a hosted checkout page works: an application doesn’t embed the wallet and doesn’t ask it for keys, it sends the user to the gateway and gets a result back. If a wallet isn’t a browser extension, applications need some other way to reach it — and the answer is the pattern the web settled twenty years ago: redirects.&lt;/p&gt;
&lt;p&gt;A client-side SDK handles it, with three moves. &lt;strong&gt;Signup&lt;/strong&gt; sends a new user off to create an account and returns with their address and nickname. &lt;strong&gt;Connector&lt;/strong&gt; is log-in: the application gets back who it’s now talking to. &lt;strong&gt;Authorizer&lt;/strong&gt; hands over an unsigned transaction, the user reviews and approves it on Sovrano, and it comes back signed, ready to broadcast. Each of the three produces a URL to send the user to, and knows how to read the response when they come back. Anyone who has integrated OAuth recognises every step, which was exactly the goal.&lt;/p&gt;
&lt;p&gt;The part that satisfies me most is that the Sovrano Wallet app uses that same SDK. There’s no privileged channel reserved for the official wallet: the wallet is a client of the gateway like any other application, going through the same public interface a third party would use.&lt;/p&gt;
&lt;p&gt;Under the hood, an approved action is two operations in one transaction: a pre-authorization that records exactly what was approved — contract, method, arguments — and the execution, which only goes through if it matches. Approve once, spend once.&lt;/p&gt;
&lt;h2 id=&quot;telling-the-user-what-theyre-signing&quot;&gt;Telling the user what they’re signing&lt;/h2&gt;
&lt;p&gt;There’s a problem inside that authorization step, and it isn’t cryptographic. A blockchain operation is a contract address, an entry point, and a blob of encoded arguments. Asking someone to approve &lt;em&gt;that&lt;/em&gt; is not consent, it’s a formality with extra steps — the “Requested operations” screen in those demos has to say something true and comprehensible, or there’s no point showing it at all.&lt;/p&gt;
&lt;p&gt;So the wallet backend carries a resolver per operation type it understands. The transfer resolver pulls the token’s metadata, formats the amount with the right decimals, and resolves the sender and recipient nicknames, so the screen can say “5 KOIN to @someone” instead of rendering base58. Others cover installing and uninstalling modules, registering and revoking a passkey, registering and revoking an OpenID identity, and specific actions from the applications built on it.&lt;/p&gt;
&lt;p&gt;And then there’s the default resolver, for operations nothing recognises. That fallback is the honest bit: a wallet cannot decode a contract it has never heard of, and the right response is to say so plainly rather than dress an unknown call up in reassuring language.&lt;/p&gt;
&lt;p&gt;The rest of the app follows from having nicknames: pay &lt;code&gt;@someone&lt;/code&gt;, request money from &lt;code&gt;@someone&lt;/code&gt;, a list of activity, somewhere to put idle funds. Ordinary shapes, which was the ambition.&lt;/p&gt;
&lt;video controls preload=&quot;metadata&quot; poster=&quot;https://adrianofoschi.com/videos/blog/sovrano-requested-operations-poster.jpg&quot; aria-label=&quot;Authorizing X access from the native app, then paying a nickname: searching, picking the recipient, and the Requested operations screen that follows&quot; style=&quot;width:100%;aspect-ratio:16/9;border:1px solid var(--color-border);border-radius:0.375rem&quot;&gt;
	&lt;source src=&quot;https://adrianofoschi.com/videos/blog/sovrano-requested-operations.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;p&gt;&lt;em&gt;Paying &lt;code&gt;@adrianofoschi&lt;/code&gt;: search the nickname, and the same “Requested operations” screen resolves it before you sign.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-holds&quot;&gt;What holds&lt;/h2&gt;
&lt;p&gt;What I’d defend is the premise. Every barrier I set out to remove came off: account creation is a fingerprint or an account you already have, transactions cost nothing, there’s no extension to install, and the seed phrase is a fallback you have to go looking for. None of that required weakening self-custody, because the account itself is programmable enough to hold the rules. Nobody else holds the keys: the user’s own device does, and what can happen with them is decided by a contract that belongs to them.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>No system call for that: getting passkey and JWT signatures verified on chain</title><link>https://adrianofoschi.com/blog/onchain-signature-verifiers/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/onchain-signature-verifiers/</guid><description>A smart contract that wants to accept a fingerprint or a Google login has to verify signature formats the blockchain knows nothing about. Writing that verification myself cost too much to run, so the answer turned out to be knowing where to look for code that already fits inside a constrained VM.</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every blockchain hands contracts a short, fixed menu of cryptography. Usually it’s exactly one curve: the one the chain’s own accounts are built on, wired in as a system call, and nothing else.&lt;/p&gt;
&lt;p&gt;That’s fine right up until you want an account controlled by something people actually own. A passkey signs with ECDSA over the NIST P-256 curve. An OpenID Connect identity token issued by Google or Microsoft is signed RS256 — RSA with SHA-256. On &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt;, a contract can verify neither: the system call doesn’t exist, so there’s nothing to call.&lt;/p&gt;
&lt;p&gt;If you want smart accounts controlled by a fingerprint, or by an account someone already has, that gap is the whole problem. This is how I closed it.&lt;/p&gt;
&lt;h2 id=&quot;the-first-attempt-writing-it&quot;&gt;The first attempt: writing it&lt;/h2&gt;
&lt;p&gt;The first attempt was to write it. Contracts on Koinos are written in AssemblyScript — a subset of TypeScript that compiles to WebAssembly — so the shortest road was to implement the verification there, in the same language as everything else.&lt;/p&gt;
&lt;p&gt;It worked, and it wasn’t usable. There are no fees on this chain: every transaction consumes mana, a metered budget, and there’s a ceiling on how much of it one can burn. Verifying a signature is arithmetic on enormous numbers — modular exponentiations, curve multiplications — and that arithmetic written in AssemblyScript cost too much. A single verification ate an amount of resources that made doing it on every operation impractical. And on every operation is exactly when it’s needed.&lt;/p&gt;
&lt;p&gt;The problem wasn’t correctness, it was cost. Which changes the question: not “how do I write this”, but “where do I find an implementation already written to run where resources are scarce”.&lt;/p&gt;
&lt;h2 id=&quot;where-to-find-cryptography-that-fits-in-a-vm&quot;&gt;Where to find cryptography that fits in a VM&lt;/h2&gt;
&lt;p&gt;There’s a second reason not to write it, independent of cost. These algorithms have decades of subtle ways to go wrong, and a mistake doesn’t crash anything: it makes the contract accept a signature it should have rejected. You find out when somebody takes advantage of it.&lt;/p&gt;
&lt;p&gt;So I was looking for an implementation already written, already used in production, that could run under a contract’s constraints: no operating system, no dynamic memory allocation, and compiled code that has to stay small.&lt;/p&gt;
&lt;p&gt;Those are a bootloader’s constraints.&lt;/p&gt;
&lt;p&gt;For P-256 I took &lt;a href=&quot;https://bearssl.org/&quot;&gt;BearSSL&lt;/a&gt;’s ECDSA verifier, by way of &lt;a href=&quot;https://github.com/oreparaz/p256&quot;&gt;&lt;code&gt;oreparaz/p256&lt;/code&gt;&lt;/a&gt;, which reduces it to a single header file; BearSSL is written for embedded systems. For RSA I took the implementation from Chrome OS verified boot — the code that checks the operating system’s signature at startup — and &lt;a href=&quot;https://github.com/adrianofoschi/rsa-verify&quot;&gt;forked it&lt;/a&gt; to verify JWTs before porting it to the chain.&lt;/p&gt;
&lt;p&gt;Verified boot is the clearer case of the two. A bootloader verifies signatures and never produces one, runs before an operating system exists, doesn’t allocate memory, and has to fit in very little space: the same four things a contract has to do. Someone had already solved the problem under my exact constraints, a decade earlier, for reasons that have nothing to do with any of this.&lt;/p&gt;
&lt;p&gt;The work was getting it into the chain: that C inside a C++ contract compiled to WebAssembly with the Koinos toolchain, and small enough to deploy.&lt;/p&gt;
&lt;h2 id=&quot;make-it-work-off-the-chain-first&quot;&gt;Make it work off the chain first&lt;/h2&gt;
&lt;p&gt;Debugging a contract inside a VM is slow and blind: you see that the verification answered no, and you don’t know why. So before putting anything on chain, I got a real Google token verifying in C, on my laptop.&lt;/p&gt;
&lt;p&gt;Mostly it was there to separate two kinds of error. When a verification fails, the problem is either in the port or in the data being handed to it, and the data is the treacherous part. A JWT has to be taken apart into its three pieces, the signature is encoded in a variant of base64 that isn’t base64, and it has to be decoded correctly before it’s any use. A 2048-bit RSA signature is exactly 256 bytes long: if it isn’t after decoding, there’s no point going further, because everything after that will fail for the wrong reason.&lt;/p&gt;
&lt;p&gt;With the same code running on a laptop, that answer arrives in a second. Only then is it worth compiling anything to WebAssembly.&lt;/p&gt;
&lt;h2 id=&quot;the-contract-is-almost-nothing&quot;&gt;The contract is almost nothing&lt;/h2&gt;
&lt;p&gt;The contract itself is the smallest part. Each of the two wraps its imported header in about a hundred lines: one entry point, three arguments — signature, public key, message — and a yes or a no coming out. No state, no stored data, no authority: a pure function that happens to live on a blockchain.&lt;/p&gt;
&lt;p&gt;The interesting part is what goes in and what doesn’t, because every choice is the same choice: take work away from the chain.&lt;/p&gt;
&lt;p&gt;The public key the RSA contract expects isn’t the key the provider hands you. It’s a prepared version, holding the modulus plus a constant computed in advance to make the multiplications fast. Verified boot has that computed by whoever prepares the key, rather than by the device doing the verifying — a decision made for bootloaders that is worth even more here. The sum is done once, outside, and what’s left for the contract is only the part that can’t be avoided. Same logic for the message: the contract receives a 32-byte digest, not the token, because hashing a few hundred bytes has no business happening on chain.&lt;/p&gt;
&lt;img alt=&quot;Split of RSA verification work: the public key is prepared and the token hashed off chain, leaving the contract only a modular exponentiation and a byte-by-byte check of the rebuilt padded block&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/onchain-signature-verifiers-0.svg&quot; width=&quot;865&quot; height=&quot;1042&quot; /&gt;
&lt;p&gt;&lt;em&gt;Everything that can be computed in advance is computed off the chain. What is left inside the box is the part that cannot be avoided.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One thing that can’t be moved out is the padding check. Verifying an RSA signature doesn’t just mean comparing two numbers: the signed block has to be rebuilt and checked byte by byte. That’s where forged signatures hide, and skipping the check means accepting them.&lt;/p&gt;
&lt;p&gt;The same idea holds on the P-256 side, and it shows in the arguments: the contract doesn’t receive the passkey’s response and doesn’t parse it. It receives exactly the bytes the browser signed, assembled outside.&lt;/p&gt;
&lt;img alt=&quot;Sequence diagram of a passkey signature check: the account asks the sign module, which looks up the registered credential and delegates the maths to a P-256 verifier before answering valid&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/onchain-signature-verifiers-1.svg&quot; width=&quot;1038&quot; height=&quot;871&quot; /&gt;
&lt;p&gt;&lt;em&gt;The verifier’s whole job, at the right-hand end of the flow: three arguments in, &lt;code&gt;true&lt;/code&gt; out.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No dynamic allocation anywhere, fixed-size buffers throughout. The two contracts are the same file with a different verifier inside.&lt;/p&gt;
&lt;p&gt;Two stateless contracts, each answering a single question, and from there on any smart account on the chain can be controlled by a passkey or by a token issued by an identity provider — with the check happening on the chain, instead of a server saying “trust me, I verified it”.&lt;/p&gt;
&lt;p&gt;What I take away isn’t the code, it’s where I found it — and why there. When you need a cryptographic primitive inside a VM with few resources, the useful implementations aren’t in the server-side libraries you reach for first: they’re in bootloaders, in embedded TLS stacks, in firmware. The people who wrote them were working under the same constraint that had stopped me here — do as little as possible, with as little as possible — and the decisions that fall out of it, like precomputing everything that can be precomputed somewhere else, hold on a blockchain exactly as they held at the start of a computer.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>Veive: turning a blockchain account into something you can program</title><link>https://adrianofoschi.com/blog/veive-smart-accounts/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/veive-smart-accounts/</guid><description>In 2024 I built a modular smart account protocol for the Koinos blockchain. The absence of the piece everyone else builds on turned out to produce a stronger authorization model than the one I was copying.</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On most blockchains, an account is a key pair. Not a record, not an object with rules — a public key you can be paid at, and a private key that can do absolutely anything with what’s there.&lt;/p&gt;
&lt;p&gt;Almost every complaint about using a blockchain follows from that one fact. There is no recovery, because there is nothing to recover to: lose the key and the account is gone, and no one can help you. There are no permissions, because a key has nothing to reason with — an account can either do everything or nothing. No spending limit, no second approver, no letting an application move exactly one token and nothing else. And onboarding always terminates in the same instruction: write down these twelve words, keep them forever, tell no one. All of these are symptoms of the same cause: there’s no account capable of running its own logic — not just signing or refusing, but reacting, automating, doing things on its own behalf.&lt;/p&gt;
&lt;p&gt;Ethereum’s answer is account abstraction: make the account a smart contract, so it can decide for itself what a valid signature is and what an operation is allowed to do. &lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-4337&quot;&gt;ERC-4337&lt;/a&gt; established the pattern, and &lt;a href=&quot;https://eips.ethereum.org/EIPS/eip-7579&quot;&gt;ERC-7579&lt;/a&gt; standardized &lt;em&gt;modular&lt;/em&gt; smart accounts — accounts you extend by installing modules.&lt;/p&gt;
&lt;p&gt;Koinos, a blockchain with no transaction fees, had no account abstraction at all. I set out to build it, and called it &lt;a href=&quot;https://github.com/veive-io&quot;&gt;Veive&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;an-account-made-of-parts&quot;&gt;An account made of parts&lt;/h2&gt;
&lt;p&gt;A Veive account is a contract that does very little by itself. It’s a container. Behaviour arrives as &lt;a href=&quot;https://veive-io.github.io/framework/module-types/&quot;&gt;modules&lt;/a&gt; you install into it, and there are four kinds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://veive-io.github.io/framework/module-types/validation-modules/&quot;&gt;Validation modules&lt;/a&gt;&lt;/strong&gt; decide whether an operation is authorized. This is policy: a valid signature, multiple approvers, a pre-authorized allowance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://veive-io.github.io/framework/module-types/execution-modules/&quot;&gt;Execution modules&lt;/a&gt;&lt;/strong&gt; carry an operation out — and, because they’re just code, they can do more than the operation literally asked for.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://veive-io.github.io/framework/module-types/sign-modules/&quot;&gt;Sign modules&lt;/a&gt;&lt;/strong&gt; define &lt;em&gt;how you prove it’s you&lt;/em&gt; — a seed phrase, a passkey, an identity provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://veive-io.github.io/framework/module-types/hooks-modules/&quot;&gt;Hook modules&lt;/a&gt;&lt;/strong&gt; run before and after an operation. Pre-hooks can check conditions and block; post-hooks can log, notify, or trigger follow-up work.&lt;/li&gt;
&lt;/ul&gt;
&lt;img alt=&quot;Sequence diagram of one operation passing through the account: validation, then a pre-hook, then execution, then a post-hook, with the account calling each module in turn&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/veive-smart-accounts-0.svg&quot; width=&quot;1128&quot; height=&quot;1068&quot; /&gt;
&lt;p&gt;&lt;em&gt;The flow of a single operation through validation, a pre-hook, execution, and a post-hook — the account calls each module type in turn, and every one of them can stop it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It’s tempting to read that list as four flavours of permission check. It isn’t. Only the first one is about saying no.&lt;/p&gt;
&lt;p&gt;The split I’d defend hardest is the third one. In ERC-7579 the validator both authenticates and authorizes. Separating the &lt;em&gt;signature method&lt;/em&gt; from the &lt;em&gt;authorization policy&lt;/em&gt; means they can vary independently: you can move an account from a seed phrase to a fingerprint to a Google login without touching a single rule about what that account is permitted to do. Authentication is a mechanism, policy is a decision, and they change on completely different schedules.&lt;/p&gt;
&lt;h2 id=&quot;an-operation-is-a-specific-thing&quot;&gt;An operation is a specific thing&lt;/h2&gt;
&lt;p&gt;ERC-7579 is deliberately vague about what an “operation” is. Veive pins it down: an operation is a &lt;code&gt;contract_id&lt;/code&gt;, an &lt;code&gt;entry_point&lt;/code&gt;, and its &lt;code&gt;args&lt;/code&gt; — this contract, this method, these arguments.&lt;/p&gt;
&lt;p&gt;Being concrete there is what makes fine-grained policy expressible at all. “Require multiple signatures for &lt;code&gt;transfer&lt;/code&gt; on the KOIN contract, and a plain signature for everything else” is only a sentence you can write if the system knows what a call to a specific method of a specific contract &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-missing-chokepoint&quot;&gt;The missing chokepoint&lt;/h2&gt;
&lt;p&gt;The one piece of ERC-4337 I could not copy was its EntryPoint — and not copying it is what made the result stronger.&lt;/p&gt;
&lt;p&gt;ERC-4337 routes everything through a singleton EntryPoint contract. Every user operation goes in there, gets validated, and comes out the other side. It’s a chokepoint, and chokepoints are convenient: one place to stand and check things.&lt;/p&gt;
&lt;p&gt;Koinos has no EntryPoint. Operations reach accounts directly. My first read was that this was a gap I’d have to work around.&lt;/p&gt;
&lt;p&gt;What Koinos has instead is a native &lt;code&gt;authorize&lt;/code&gt; hook: when a call needs an account’s authority, the chain asks that account, directly, whether it authorizes it. Veive implements that hook, and the consequence is better than the thing I was copying.&lt;/p&gt;
&lt;p&gt;An EntryPoint only sees what passes through the EntryPoint. The chain’s own authority mechanism sees &lt;em&gt;everything&lt;/em&gt; — including operations generated internally by other contracts partway through a transaction. So when a contract, in the middle of doing something you did approve, tries to slip in a token transfer from your account that you didn’t, your validation modules are consulted for that too, and it fails. Not because I added a defence against it, but because validation is wired into the layer that actually knows about every call, rather than into a lane that well-behaved traffic agrees to use.&lt;/p&gt;
&lt;p&gt;The same hook covers the chain’s other authorization types, so an account’s policies also govern uploading contract code and applying a transaction — not just calls. And there’s one small guard I’m fond of: if the caller asking for authorization &lt;em&gt;is&lt;/em&gt; the validation module itself, the answer is always no. A module cannot vote itself into power.&lt;/p&gt;
&lt;h2 id=&quot;scopes-picking-the-right-module-at-the-right-moment&quot;&gt;Scopes: picking the right module at the right moment&lt;/h2&gt;
&lt;p&gt;If an account can hold many modules, something has to decide which one applies to the operation in front of it. Veive resolves that through scopes, at three levels of specificity:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;entry_point&lt;/code&gt; &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;contract_id&lt;/code&gt; — this method on this contract&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entry_point&lt;/code&gt; — this method anywhere&lt;/li&gt;
&lt;li&gt;any operation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Resolution runs most-specific-first, and the rules differ per module type, which is the detail that makes the whole thing behave sensibly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Validators&lt;/strong&gt;: the single most specific match wins. Exactly one policy decides, so there’s never an ambiguous verdict.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executors and hooks&lt;/strong&gt;: everything that matches runs. These compose; a logging hook and a rate-limit hook have no reason to exclude each other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sign modules&lt;/strong&gt;: one, account-wide. Two simultaneous ways to prove identity is two attack surfaces, and the whole point of the account is that identity has a single answer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Different resolution strategies for different module types sounds like an inconsistency. It’s the opposite: a decision must be unambiguous, side effects should accumulate, and identity should be singular. The dispatch matches what each kind of module is actually for.&lt;/p&gt;
&lt;h2 id=&quot;what-you-actually-build-with-it&quot;&gt;What you actually build with it&lt;/h2&gt;
&lt;p&gt;The reason all of this matters isn’t the taxonomy — it’s that behaviour people normally expect from a bank lands inside the account, where no application can route around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A piggybank.&lt;/strong&gt; An execution module installed in the &lt;code&gt;transfer&lt;/code&gt; scope that puts a percentage of every outgoing transfer into a savings account. Not a feature of a wallet app, not a service you sign up for: the account does it. Every transfer, from any application, from any device, including ones that have never heard of the module.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A spending limit.&lt;/strong&gt; A pre-hook that inspects the operation before it runs and blocks it if it exceeds a cap. The cap belongs to the account, so it applies to every path into that account rather than to the one interface that bothered to implement it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A notification.&lt;/strong&gt; A post-hook that fires after significant transfers. Because hooks are scoped like everything else, “significant” can mean one thing for a specific token contract and something else globally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A pre-authorized allowance.&lt;/strong&gt; &lt;a href=&quot;https://veive-io.github.io/framework/core-modules/mod-allowance/&quot;&gt;&lt;code&gt;mod-allowance&lt;/code&gt;&lt;/a&gt; is a validation module that inverts the usual order: you approve an exact operation in advance — contract, method, arguments, transaction — and store it. When the operation later arrives, it’s checked against what you approved, and on a match the allowance is &lt;em&gt;consumed&lt;/em&gt;. Approving something once means it can happen once; a replayed transaction finds nothing left to spend. And since Veive validates internally generated operations too, this covers the calls a contract makes on your behalf partway through doing something else, not just the ones you sent yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recovery through guardians.&lt;/strong&gt; The same multisig module used for shared ownership also works for recovery: installed in the module install/uninstall scopes, a group of guardians — with a configurable threshold, say two of three — can authorize replacing a lost signing method, like a lost device’s WebAuthn key. No external recovery service is needed: it’s the same validation mechanism, applied to a different scope.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple signatures.&lt;/strong&gt; A validation module requiring several approvers for operations in a given scope, while everything outside that scope carries on with a single signature.&lt;/p&gt;
&lt;p&gt;None of these need the applications to cooperate, and none of them need the account to be rewritten. Install, and the account behaves differently from that block onward.&lt;/p&gt;
&lt;h2 id=&quot;passkeys-and-google-accounts-verified-on-chain&quot;&gt;Passkeys and Google accounts, verified on chain&lt;/h2&gt;
&lt;p&gt;The sign modules are where the abstraction stops being theory.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://veive-io.github.io/framework/core-modules/mod-sign-webauthn/&quot;&gt;&lt;code&gt;mod-sign-webauthn&lt;/code&gt;&lt;/a&gt; lets an account be controlled by a &lt;a href=&quot;https://www.w3.org/TR/webauthn-2/&quot;&gt;WebAuthn&lt;/a&gt; credential — a passkey. You register the credential’s public key with the account, and from then on a fingerprint or a face signs transactions. No seed phrase exists anywhere.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/veive-io/mod-sign-openid-as&quot;&gt;&lt;code&gt;mod-sign-openid&lt;/code&gt;&lt;/a&gt; goes further: you link an &lt;a href=&quot;https://openid.net/developers/how-connect-works/&quot;&gt;OpenID Connect&lt;/a&gt; identity — a Google or Microsoft account — and the account verifies the provider’s ID token itself. On chain, the contract decodes the JWT, checks it against the provider’s public key, and reads the claims to confirm the operation was authorized by that user. A person who has never heard of a blockchain signs in with the account they already have.&lt;/p&gt;
&lt;p&gt;Neither of those was possible with what the chain provided. WebAuthn signs with ECDSA over P-256; OpenID ID tokens are signed RS256. Koinos could verify neither, so both primitives had to be brought on chain as standalone verifier contracts, written in C++ and compiled to WebAssembly, exposing a single method: given a signature, a public key and a message, is this valid?&lt;/p&gt;
&lt;p&gt;The cryptography itself I didn’t write, and there’s no reason anyone should — P-256 verification came from BearSSL’s single-file ECDSA verifier, and RSA from the implementation in Chrome OS’s verified boot, &lt;a href=&quot;https://github.com/adrianofoschi/rsa-verify&quot;&gt;which I forked&lt;/a&gt; to work through JWT verification before porting it. The work was getting battle-tested C into a blockchain VM: onto the Koinos C++ SDK, through the WASI toolchain, and small enough to deploy.&lt;/p&gt;
&lt;p&gt;A nickname registry is the last piece of the barrier. An account you reach by &lt;code&gt;@name&lt;/code&gt;, open with a fingerprint, on a chain where transactions cost nothing, is an account a normal person can be handed. No extension to install, no tokens to buy first, no secret to guard for the rest of their life.&lt;/p&gt;
&lt;h2 id=&quot;where-it-stands&quot;&gt;Where it stands&lt;/h2&gt;
&lt;p&gt;The module packages are published on npm under MIT — validation, execution, sign and hooks base libraries, plus concrete modules for mnemonic, WebAuthn and OpenID signing, multisig validation, and pre-authorized allowances. They’re installable.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Veive documentation site, on the Framework page: a sidebar covering the protocol, use-cases, module types and roadmap, and a body explaining modular smart accounts inspired by ERC-7579&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;1397&quot; src=&quot;https://adrianofoschi.com/_astro/docs-site.DL293XiO_17sqN9.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The documentation site, &lt;a href=&quot;https://veive-io.github.io/&quot;&gt;rebuilt and republished on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;What I don’t discount is the design. The constraint I thought was a deficiency — no EntryPoint to build on — is exactly what forced validation down into the layer where every call is visible, and produced authorization coverage the pattern I was copying doesn’t have.&lt;/p&gt;
</content:encoded><category>blockchain</category><category>architecture</category></item><item><title>The freelance years: real clients, and a lesson school never taught me</title><link>https://adrianofoschi.com/blog/freelance-years/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/freelance-years/</guid><description>Freelancing was the first time nobody vouched for the work but me — and the first time I learned that technical skill alone isn&apos;t enough.</description><pubDate>Mon, 16 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Freelancing, from 2013 to 2015, was the first time nobody else’s name was attached to the work — no principal assigning it, no institution funding it, no program structuring it. No one was vouching for me. If a client trusted the work, it was because of the work itself, not because something else stood behind it.&lt;/p&gt;
&lt;h2 id=&quot;finding-clients-two-different-ways&quot;&gt;Finding clients, two different ways&lt;/h2&gt;
&lt;p&gt;I worked both ends of it: bidding for projects on Freelancer.com, competing with people I’d never see, entirely on a portfolio and a proposal — and local clients, found the older way, through word of mouth. The two were almost opposite experiences. On Freelancer.com you were a line item in a list of quotes, judged on price and past reviews before anyone read a word you wrote. Locally, you were a person someone’s cousin recommended, and the first meeting mattered more than the portfolio did.&lt;/p&gt;
&lt;h2 id=&quot;what-the-work-actually-was&quot;&gt;What the work actually was&lt;/h2&gt;
&lt;p&gt;It wasn’t one kind of project. Some of it was commercial websites — a WordPress theme built for a studio, a site for a small brand, the kind of work that’s mostly about understanding what a business actually needs to show people, not just building a page. Some of it was heavier: management software for real operations, complex enough to need actual design before any code — I built one for a nightclub’s day-to-day bookings and administration, in a Node.js framework called Feathers, which at the time was still a fairly deliberate choice. I also built and shipped small standalone tools, like a QR code generator site people could use directly, no client relationship required at all.&lt;/p&gt;
&lt;h2 id=&quot;the-lesson-that-mattered-more-than-any-of-it&quot;&gt;The lesson that mattered more than any of it&lt;/h2&gt;
&lt;p&gt;If I had to pick one thing this period taught me that four years of a CS degree hadn’t: technical skill gets you in the room, but it’s not what keeps a client relationship working. Understanding what someone actually needs — which is very often not exactly what they first ask for — and managing expectations honestly when reality doesn’t match the timeline or the budget, matters just as much as the code itself. Nobody grades that. You just find out the hard way when a project that was technically fine still left a client unhappy, or a project that had real problems still ended well because you communicated clearly through it.&lt;/p&gt;
&lt;p&gt;That’s the part no classroom ever tested, and the part freelancing forced on me directly.&lt;/p&gt;
</content:encoded><category>career</category></item><item><title>Graded by someone who didn&apos;t understand what I&apos;d built</title><link>https://adrianofoschi.com/blog/graded-by-someone-who-didnt-understand/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/graded-by-someone-who-didnt-understand/</guid><description>A university exam graded on usability and security, a BlaBlaCar clone built with an early MVVM framework, and the grade that revealed the professor hadn&apos;t understood what he was looking at.</description><pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I started a Computer Science degree in 2009 with a very weak background in mathematics. It had never been my subject, and the degree’s math courses made that clear immediately: I ended up repeating several of them, struggling to keep pace while other students seemed to move through without much effort. For a couple of years I felt like I was always one step behind — not from lack of effort, but because I was missing foundations other people already had.&lt;/p&gt;
&lt;p&gt;Not all of the math was an obstacle, though. Discrete mathematics, of all things, was one of the courses that opened my mind the most from a logical standpoint: sets, relations, graphs, propositional logic — a way of thinking that felt closer to how I already reasoned when writing code, and that course was the first to make it explicit. The Programming Languages course was one of the most interesting of the whole degree too, for a different reason: not so much its difficulty, but the pleasure of seeing formalized what I had so far only worked out by intuition.&lt;/p&gt;
&lt;h2 id=&quot;the-web-programming-exam-usability-and-security&quot;&gt;The Web Programming exam: usability and security&lt;/h2&gt;
&lt;p&gt;One exam I was looking forward to more than most, near the end of the degree, was Web Programming. The assignment was simple to state and anything but simple to do well: build a web project — any web application — and document the work. The two requirements that mattered most were usability and security: avoiding, at the most basic level, mistakes like SQL injection. In practice, most of my classmates turned in projects that either didn’t work or were riddled with elementary security holes, built on the same PHP-LAMP examples that were being copy-pasted from site to site at the time.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built-an-mvvm-frontend-and-a-blablacar-clone&quot;&gt;What I built: an MVVM frontend and a BlaBlaCar clone&lt;/h2&gt;
&lt;p&gt;I decided to do things differently. On the server side I chose a PHP micro-framework designed specifically to guarantee a high level of security by default, instead of hand-writing queries and routing the way almost everyone else did. On the frontend I used &lt;a href=&quot;https://knockoutjs.com/&quot;&gt;Knockout.js&lt;/a&gt; — an MVVM (Model-View-ViewModel) framework that predated Angular and React by a few years, one of the first to bring automatic binding between data and interface to the web without updating the DOM by hand, line by line. With those two pieces I built a working clone of BlaBlaCar: ride search, bookings, user management, and a modern, responsive interface — far from a given at the time, when most university projects stopped at a fixed layout designed only for the lab monitor.&lt;/p&gt;
&lt;h2 id=&quot;a-20-and-a-professor-who-didnt-know-what-mvvm-was&quot;&gt;A 20, and a professor who didn’t know what MVVM was&lt;/h2&gt;
&lt;p&gt;The final grade was 20 out of 30. Very low, especially next to what I’d seen other students submit. I went to ask the professor for an explanation, expecting some technical point I’d missed. Instead, from the conversation, I noticed something else: he hadn’t actually understood what an MVVM framework was, or what it meant in practice to separate logic from data and view that way. His level of knowledge about what I had built was, simply, lower than mine. It was a hard thing to take in — not so much the grade itself, but discovering that whoever was judging me didn’t have the tools to do it. I absorbed it anyway.&lt;/p&gt;
&lt;h2 id=&quot;the-opposite-result-in-physics&quot;&gt;The opposite result, in Physics&lt;/h2&gt;
&lt;p&gt;The flip side came from another exam around the same period: Physics. It was a subject I had no great hopes for — I felt as distant from it as from pure mathematics, maybe more. I sat the exam without expecting much. The result was 30 out of 30 with honors, the highest grade I could imagine, exactly where I expected the least.&lt;/p&gt;
&lt;p&gt;Put side by side, those two grades don’t tell a coherent story, and I think that’s exactly the point. A 20 on the work I was proudest of, a 30 with honors where I expected nothing: the grade wasn’t a reliable measure of either the quality of what I’d done or my actual understanding of the subject. Sometimes the person judging doesn’t have the tools to see what’s in front of them, and the merit stays invisible. Other times it goes the other way, and you get rewarded beyond any expectation. I learned early not to put too much weight on a single number — and to trust more in what I actually knew I had built.&lt;/p&gt;
</content:encoded><category>early-projects</category></item><item><title>A prediction game with no fees, and the server that keeps time for it</title><link>https://adrianofoschi.com/blog/blockchain-cant-tell-the-time/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/blockchain-cant-tell-the-time/</guid><description>A prediction game only works if a one-dollar bet is worth placing, which rules out any chain that charges fees. Koinos charges none — but it also has no oracle, so the clock and the price come from a server I wrote, and that server is the trust model.</description><pubDate>Tue, 11 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Prediction games have been popular for a while now. The format is always the same: you bet on how something will turn out, and whoever gets it right splits the money of whoever got it wrong. It works because it asks you to know nothing about finance, only to have an opinion about where a price is going over the next few hours.&lt;/p&gt;
&lt;p&gt;What I built is the minimal version. A round opens on a currency pair. You put some tokens on &lt;strong&gt;long&lt;/strong&gt; or &lt;strong&gt;short&lt;/strong&gt;. Betting closes, the round runs, and at the end the price is compared against where it started. The winning side splits the pot; the losing side filled it. It’s a prototype, running on testnet, built on &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt; — a blockchain with no transaction fees.&lt;/p&gt;
&lt;p&gt;Fees are the first thing that makes a game like this impossible. On a chain with gas, a one-dollar bet costs more in fees than it can win: the stake has to be big for the toll to be worth paying. Remove the fee and small stakes become possible again, so rounds can be short and casual instead of long and serious.&lt;/p&gt;
&lt;p&gt;But the real friction comes earlier, and fees are only its last line. To bet one dollar, someone has to install a wallet, put twelve words somewhere safe, open an account on a centralized exchange, verify their identity, buy the token, work out which network they’re buying it on, and send it to their own address. Then they can bet their dollar. Nobody walks that whole road for a game. The chain took the fees away; the rest of the road, this prototype doesn’t touch.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Two phone screens: a pool chooser listing BTC/USD 4h as live with ETH/USD and SOL/USD coming soon, and a betting screen showing the current BTC price, a countdown reading “betting stops in 11m 10s”, payout percentages for each side, and Long and Short buttons&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;637&quot; height=&quot;616&quot; src=&quot;https://adrianofoschi.com/_astro/prediction-screens.LyRnh6nT_Zm9pS5.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Choosing a pool, and betting in one. The two payout figures move as money lands on either side.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-contract-was-the-easy-part&quot;&gt;The contract was the easy part&lt;/h2&gt;
&lt;p&gt;The on-chain half is unremarkable, which is the right outcome. A round has a state — open, in progress, closed, or one of two failure states — and users’ bets hang off it. Three operations matter: entering by putting money on a side, collecting if you won, taking your stake back when a round can’t be settled. Everything else is bookkeeping.&lt;/p&gt;
&lt;p&gt;Payouts are pari-mutuel, not fixed odds. There’s no house on the other side of your bet: the winners divide what the losers staked, in proportion to what each put in. That’s why the two percentages on the betting screen keep moving — they aren’t a price the game is offering you, they’re the current shape of the pot.&lt;/p&gt;
&lt;p&gt;Written and deployed, that’s a few hundred lines of AssemblyScript. And it does nothing at all, forever, because of two things a blockchain cannot do.&lt;/p&gt;
&lt;h2 id=&quot;koinos-has-no-oracle&quot;&gt;Koinos has no oracle&lt;/h2&gt;
&lt;p&gt;An oracle is the piece that connects a blockchain to the world outside, and it’s needed for two distinct things that often get conflated.&lt;/p&gt;
&lt;p&gt;The first is time. Nothing happens on a blockchain unless someone sends a transaction: there’s no scheduler, no cron. A contract reacts, it never acts. So “betting closes in eleven minutes” isn’t something the contract can enforce by itself — somebody has to show up at the right moment and tell it to change state: open the next round, close betting on this one, settle that one.&lt;/p&gt;
&lt;p&gt;The second is facts. A contract can’t go and read the price of Bitcoin: it has no network access, and it must not have any. Every node executing that transaction has to arrive at an identical result, and two nodes calling an API a second apart would not. Determinism is the basis of consensus, and reaching outside is how you break it. So the price has to be handed to the contract from outside.&lt;/p&gt;
&lt;p&gt;On bigger chains this service already exists as infrastructure in its own right, shared by whoever needs it. On Koinos it doesn’t. So I implemented it: an external server that reads the state of the current round, works out which transition is due, asks &lt;a href=&quot;https://docs.cdp.coinbase.com/exchange/reference/exchangerestapi_getproductticker&quot;&gt;Coinbase’s public API&lt;/a&gt; for the pair’s rate, and sends the transaction that moves the round forward, carrying the price along as an argument.&lt;/p&gt;
&lt;img alt=&quot;One round transition: the server reads the contract state, works out which transition is due, asks Coinbase for the pair&apos;s rate, and sends the transaction that moves the round on with the price carried as an argument&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://adrianofoschi.com/d2/blog/blockchain-cant-tell-the-time-0.svg&quot; width=&quot;749&quot; height=&quot;741&quot; /&gt;
&lt;p&gt;&lt;em&gt;The rules sit in the contract and nobody can change them. The clock and the price arrive from
one process, one key and one exchange’s endpoint — which is where the trust actually sits.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-server-is-a-point-of-trust&quot;&gt;The server is a point of trust&lt;/h2&gt;
&lt;p&gt;The rules of the game are on chain and nobody can change them. The clock and the price are not. If the server stops, rounds hang; if it gets the price wrong, or lies about it, the round closes badly and the chain records the result without asking questions. This isn’t trustless: the stakes are held by a contract nobody can rewrite, but settlement depends on one process, one key, and one exchange’s public endpoint.&lt;/p&gt;
&lt;p&gt;The correct solution is an oracle network: several independent reporters, staked, penalised for disagreeing. That’s an order of magnitude more work than the game itself, and it doesn’t fit inside a prototype.&lt;/p&gt;
&lt;p&gt;What does fit is making sure that when something goes wrong, the money comes back. That’s why a round has two failure states next to the successful one: if the server doesn’t return, if one side has no players, if something doesn’t add up, the round is marked skipped or suspended rather than closed, and the refund opens. Nobody loses a stake to an event that didn’t happen properly.&lt;/p&gt;
&lt;h2 id=&quot;mana-measuring-the-limit-rather-than-guessing-it&quot;&gt;Mana: measuring the limit rather than guessing it&lt;/h2&gt;
&lt;p&gt;Koinos charges no fees: it meters transactions with mana, a regenerating budget, and before sending one you have to declare how much it may consume. Estimate too low and the transaction fails, too high and you tie up more than you need.&lt;/p&gt;
&lt;p&gt;So the server sends everything twice: first as a trial run that isn’t broadcast, purely to read back how much it actually consumed, then for real with that figure plus ten percent. The limit is measured rather than guessed.&lt;/p&gt;
&lt;p&gt;What I take away from this project is a question. When someone shows me a decentralized application, I don’t ask whether it’s decentralized — almost none of them are, if you’re strict about it. I ask which parts aren’t, and what those parts can do to me on their worst day. Usually there are more of them than the pitch lets on, and almost nobody has thought about the refund.&lt;/p&gt;
&lt;h2 id=&quot;update--february-2025&quot;&gt;Update — February 2025&lt;/h2&gt;
&lt;p&gt;Since writing this I’ve started building Sovrano, a smart-account wallet you sign into with an account you already have, with no twelve words to keep safe. That’s where a game like this one should live. The missing piece isn’t the contract: it’s everything that comes before it — installing a wallet, opening an exchange account, verifying your identity, buying the token, sending it to yourself. The goal is to bring the purchase inside the wallet too, so that between wanting to bet a dollar and having bet it there’s nothing left in the way. I’m not there yet.&lt;/p&gt;
&lt;p&gt;The other direction is the game itself. Right now it can do one thing: compare a pair’s price against where it started. But the contract doesn’t need to know it’s looking at a price — it needs an event with a date and an outcome, reported from outside. Making it agnostic about the type of event means being able to open rounds on a match, on a vote, on anything with a verifiable result: what changes is who brings the result in, not the rest.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>My thesis: Hadoop vs. MySQL, and an honest result nobody expected</title><link>https://adrianofoschi.com/blog/big-data-thesis/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/big-data-thesis/</guid><description>For my computer science thesis I built the same data warehouse twice — once in Hive, once in MySQL — and benchmarked them head to head. The new technology didn&apos;t win by default.</description><pubDate>Tue, 12 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I started a computer science degree in 2009. By the time I got to my thesis in 2012, Big Data was becoming a real word, not just a buzzword yet, and I wanted my thesis to be about actually building one of these systems rather than just reading about them — a data warehouse that pulled together data from different web sources using the emerging &lt;a href=&quot;https://hadoop.apache.org/&quot;&gt;Hadoop&lt;/a&gt;/&lt;a href=&quot;https://hive.apache.org/&quot;&gt;Hive&lt;/a&gt; stack, instead of a traditional relational database.&lt;/p&gt;
&lt;h2 id=&quot;what-i-actually-built&quot;&gt;What I actually built&lt;/h2&gt;
&lt;p&gt;The dataset was geolocated tweets, cross-referenced with nearby points of interest — cafes, banks, bookstores, transit stations — each with its own category, rating and price level. The connecting concept was the “hotspot”: a public WiFi access point, with its own coordinates. The core question the model let you ask was things like: how much does a hotspot’s neighborhood get talked about, how “urbanized” is it based on how many places surround it, and what’s the weighted price level of everything nearby.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Star schema for the thesis data warehouse: a tweet fact table linked to date, status, user and hotspot dimensions, and a second fact table linking hotspot to place with rating, price level and distance&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;829&quot; height=&quot;717&quot; src=&quot;https://adrianofoschi.com/_astro/star-schema.BE_64s92_2vUpml.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The multidimensional model: tweets as one fact table, hotspot-to-place relationships as another, joined through a shared hotspot dimension.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Computing “how close is this hotspot to that place” meant calculating real-world distance from latitude/longitude pairs, so I wrote a small Java UDF — a Haversine distance function — and loaded it straight into Hive as a custom SQL function. It’s still sitting in a public repo, still compiles, still does exactly what it did in 2012.&lt;/p&gt;
&lt;h2 id=&quot;the-experiment&quot;&gt;The experiment&lt;/h2&gt;
&lt;p&gt;Building the warehouse in Hive wasn’t the actual thesis question. The real question was whether it was worth it — so I built the identical data warehouse a second time, in plain MySQL, and benchmarked both against a real sample: about 103,000 records across tweets, places, hotspots and users. Three things got measured: storage size, import time, and query response time.&lt;/p&gt;
&lt;p&gt;Two of the three results were exactly what you’d expect from the pitch. Hive’s storage came out smaller — about 8MB against MySQL’s 13.5MB for the same data — because a schema-less format doesn’t waste space on padding or null columns the way a rigid relational schema does. Import time wasn’t close: Hive loaded the entire dataset in just over 2 seconds, MySQL took over 5 minutes, because Hive is essentially writing files while MySQL is doing per-row inserts with constraint checking.&lt;/p&gt;
&lt;p&gt;Then came the query benchmarks, and the results weren’t what the pitch promised. On two of the four analytical queries — the ones combining a join with heavier aggregation and sorting — Hive was actually &lt;em&gt;slower&lt;/em&gt; than plain MySQL, not faster.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measured on ~103,000 records&lt;/th&gt;
&lt;th&gt;Hive&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage for the same data&lt;/td&gt;
&lt;td&gt;~8 MB&lt;/td&gt;
&lt;td&gt;13.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import time, whole dataset&lt;/td&gt;
&lt;td&gt;just over 2 seconds&lt;/td&gt;
&lt;td&gt;over 5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The four analytical queries&lt;/td&gt;
&lt;td&gt;slower on two of them&lt;/td&gt;
&lt;td&gt;faster on those same two&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;why-and-why-it-mattered-more-than-the-speedup-did&quot;&gt;Why, and why it mattered more than the speedup did&lt;/h2&gt;
&lt;p&gt;The honest explanation, which is also the more useful lesson: the whole benchmark ran on a single machine. Hadoop and Hive are built to win by spreading work across many nodes in parallel — take that away, and you’re left with the translation overhead of turning a query into a MapReduce job, with none of the payoff. On top of that, 103,000 records simply isn’t big data. MySQL is built to be fast at exactly that scale, and at that scale it held its own.&lt;/p&gt;
&lt;p&gt;I didn’t design the experiment to prove Hive right — I designed it to find out, and the answer was “it depends on scale, and only actually measuring it tells you.” That’s a duller conclusion than “the new thing wins,” and a much more useful one. It’s the first time a project taught me not to trust a technology’s reputation over an actual benchmark, on my own data, at my own scale — a habit that’s saved me more than once since.&lt;/p&gt;
&lt;p&gt;The thesis got a 106/110. The UDF is still on GitHub.&lt;/p&gt;
</content:encoded><category>early-projects</category></item><item><title>A migration with no undo, and 2,000 users in three weeks</title><link>https://adrianofoschi.com/blog/konio-migration-and-growth/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/konio-migration-and-growth/</guid><description>In late 2023 I rewrote the data model of a crypto wallet that was already installed on real phones, redesigned its interface, and ran a giveaway that took it past 2,000 active users in three weeks.</description><pubDate>Tue, 09 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;By autumn I had a mobile wallet in production. Konio was a native iOS and Android wallet for &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt;, a blockchain with no transaction fees, and I had written it alone: create or import a wallet from a seed phrase, hold tokens and NFTs, sign for dApps over WalletConnect, in eleven languages.&lt;/p&gt;
&lt;p&gt;It worked. The data model underneath it did not, and the next six months went into three things: fixing that, making the app comprehensible, and getting people to use it.&lt;/p&gt;
&lt;h2 id=&quot;the-data-model-i-had-shipped-was-wrong&quot;&gt;The data model I had shipped was wrong&lt;/h2&gt;
&lt;p&gt;Version one stored everything in two blobs. One JSON object in ordinary device storage held settings, accounts, coins and the address book; one encrypted object in the OS keystore held the keys and the password. Accounts carried an array of token contract ids, and those tokens lived in a separate map keyed by contract id alone.&lt;/p&gt;
&lt;p&gt;That was exactly the problem. A token isn’t a single, global thing: if I had two accounts, each with its own balance of the same token, or the same token on two different networks, to the app they were the exact same entry — because I stored it keyed only by contract id, with no record of which account or which network it belonged to. The result was that two different balances would end up overwriting each other. Every new feature I tried to add, starting with real multi-network support, ran straight into this limit.&lt;/p&gt;
&lt;p&gt;In October I rebuilt it from scratch: instead of two big blobs, five separate stores — settings, accounts, secrets, coins, contacts — where every entry carries its own unique identifier. For coins, that identifier was no longer just the contract, but the combination of account, network and contract together: exactly what a balance actually is, in reality — a specific number for a specific account on a specific network.&lt;/p&gt;
&lt;h2 id=&quot;doing-that-without-a-server&quot;&gt;Doing that without a server&lt;/h2&gt;
&lt;p&gt;There was no backend here, no backups, and no undo. The only copy of a user’s data was on their phone, and part of that data was the keys to their money. A migration that half-succeeded wouldn’t produce a support ticket, it would produce a person locked out of their own funds — permanently, because there’s no password reset for a blockchain account.&lt;/p&gt;
&lt;p&gt;Four decisions made it survivable, and I’d reuse all of them:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Migrations were dated, ordered, and committed one at a time.&lt;/strong&gt; Each one was a function keyed by a date — &lt;code&gt;20231003&lt;/code&gt;, &lt;code&gt;20231004&lt;/code&gt;, and so on — sorted and applied in sequence, with the stored version bumped only after a migration returned successfully. A failure halfway through the chain left the version at the last step that had actually worked, so the next attempt resumed at exactly the right place instead of redoing work already done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failure was loud and blocking.&lt;/strong&gt; A dedicated screen gated the app when a migration was pending, and if one threw, it showed the real error text instead of a friendly message. That looked unpolished. It was the right call: the alternative was an app that carried on with half-converted data and let the user act on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Old data got deleted one version later, not in the same step.&lt;/strong&gt; The migration that read the v1 blobs didn’t remove them. A separate, later migration did. For one release, the original data sat there untouched as a safety net — cheap insurance, paid for in a few kilobytes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small, boring migrations were fine.&lt;/strong&gt; Five shipped between October and February. One re-encoded every stored NFT token id to hex. Two just corrected a default value. Keeping them tiny is what made each one easy to reason about, and reasoning about them was the whole job.&lt;/p&gt;
&lt;h2 id=&quot;making-the-app-explain-itself&quot;&gt;Making the app explain itself&lt;/h2&gt;
&lt;p&gt;Over the same period I restructured the interface to be clearer at a glance — anonymous icons out, named sections in. The overall UX improved a lot too, especially the send and receive screens, and the whole NFT section.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The redesigned Konio assets screen: total balance in USD, a mana percentage indicator, a COINS/NFTs toggle, and a list of tokens with balances&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;298&quot; height=&quot;530&quot; src=&quot;https://adrianofoschi.com/_astro/assets-redesign.DsUq-d3a_ZGWM2W.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The redesigned main screen. The orange figure under the balance is mana.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The change I cared about most was that small orange percentage. Mana is what makes transactions on Koinos free: it’s a resource tied to the tokens you hold, spent when you transact and regenerated over time. Users had no existing mental model for it, and the temptation was to hide it — free should mean the user doesn’t have to think about it. Hiding it was worse. Someone whose transaction failed because mana ran low, with nothing on screen having warned them, concluded the app was broken. Showing “42%” and letting them tap through to a fuller explanation treated a real constraint as a real constraint.&lt;/p&gt;
&lt;h2 id=&quot;letting-the-ecosystem-add-itself&quot;&gt;Letting the ecosystem add itself&lt;/h2&gt;
&lt;p&gt;Three of the repositories weren’t code. &lt;a href=&quot;https://github.com/konio-io/konio-tokenlist&quot;&gt;&lt;code&gt;konio-tokenlist&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://github.com/konio-io/konio-dapplist&quot;&gt;&lt;code&gt;konio-dapplist&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://github.com/konio-io/konio-collectionlist&quot;&gt;&lt;code&gt;konio-collectionlist&lt;/code&gt;&lt;/a&gt; were folders of small JSON files, one per token, dApp or NFT collection, and anyone could add their own with a pull request. A token entry was a contract id, a symbol, a logo, and a price endpoint plus the path to read the number out of the response. A dApp entry was a name, a summary, tags, a URL and an icon.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Announcement graphic listing four community tokens added to the wallet: $MK, $EGG, $MARS and $OGAS&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1400&quot; height=&quot;788&quot; src=&quot;https://adrianofoschi.com/_astro/coin-listing.Cc5GeJg2_2luvle.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;A listing announcement from January 2024. Each of these arrived as a pull request against a folder of JSON files.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This was self-defense as much as openness. I was one person. Curating an in-app catalog by hand is an unbounded second job that grows exactly as fast as the thing succeeds; reviewing a ten-line JSON pull request takes two minutes. It also meant a project didn’t need my attention to appear in the wallet — they needed a text file. By January the app shipped with those lists prefilled, plus push notifications and a discovery feed that read the project’s own posts.&lt;/p&gt;
&lt;h2 id=&quot;the-giveaway&quot;&gt;The giveaway&lt;/h2&gt;
&lt;p&gt;In November I ran an onboarding campaign, and I ran it myself — the code and the marketing were the same person.&lt;/p&gt;
&lt;p&gt;It was &lt;a href=&quot;https://medium.com/@konio_io/announcing-a-koinos-onboarding-giveaway-ec528542d532&quot;&gt;announced on 12 November&lt;/a&gt; and ran from the 21st to the 27th, hosted on &lt;a href=&quot;https://zealy.io/&quot;&gt;Zealy&lt;/a&gt;, where participants complete small tasks to earn entries. The prize pool was over $3,000: a 48-piece NFT collection I wrote a smart contract for, KOIN, tokens contributed by partner projects, and &lt;a href=&quot;https://kap.domains/&quot;&gt;KAP&lt;/a&gt; name vouchers. Twelve projects from the Koinos ecosystem put prizes in. Influencers carried it to a few hundred thousand social followers.&lt;/p&gt;
&lt;p&gt;On 20 December the wallet &lt;a href=&quot;https://medium.com/@konio_io/konio-has-more-than-2000-active-users-a6356b90ac22&quot;&gt;passed 2,000 active users&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ll be honest about what that number is and isn’t. A giveaway measures reach, not retention — a meaningful share of those installs were there for the prize pool and behaved accordingly. What it did do, beyond the wallet, was pull twelve separate projects into a single coordinated push and give a small ecosystem a week of momentum it wouldn’t otherwise have had. That part I’d count as the real result.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>The blockchain that felt like web2, and the mobile wallet it was missing</title><link>https://adrianofoschi.com/blog/konio-mobile-wallet/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/konio-mobile-wallet/</guid><description>In 2023 I found a blockchain whose developer experience looked like ordinary web work, and no native mobile wallet for it. So I wrote one — and the hard part turned out to have nothing to do with cryptography.</description><pubDate>Tue, 03 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was drawn to web3, and I was looking for a blockchain that would let me build with the mindset and tools of a web2 developer — smart contracts in AssemblyScript, close to TypeScript, REST APIs instead of the usual opaque RPC endpoints. &lt;a href=&quot;https://koinos.io/&quot;&gt;Koinos&lt;/a&gt; stood out for the rest of it too: no ICO, no pre-mine, a Proof-of-Burn consensus designed to combine proof-of-stake efficiency with proof-of-work economics, and on-chain governance that upgrades the protocol by vote instead of by hard fork. But what actually convinced me was that the developer experience looked like ordinary web development.&lt;/p&gt;
&lt;p&gt;Smart contracts were written in &lt;a href=&quot;https://www.assemblyscript.org/&quot;&gt;AssemblyScript&lt;/a&gt;, a subset of TypeScript, through an &lt;a href=&quot;https://github.com/koinos/koinos-sdk-as&quot;&gt;SDK&lt;/a&gt; that compiled them to WebAssembly. The node exposed &lt;a href=&quot;https://docs.koinos.io/&quot;&gt;REST APIs&lt;/a&gt;, and there was &lt;a href=&quot;https://github.com/joticajulian/koilib&quot;&gt;&lt;code&gt;koilib&lt;/code&gt;&lt;/a&gt;, a JavaScript library that behaved like any other SDK you’d pull off npm — a genuinely good one, with APIs for pretty much anything I needed. I didn’t have to earn the right to write the first line of code by learning a new language and a new mental model first: everything I already knew from years of web work transferred directly. I’d never worked with React Native before, and this was a good excuse to get into it — there was a learning curve, but the path was clear.&lt;/p&gt;
&lt;p&gt;Once I joined the community — small, but already a few years old — I noticed something I hadn’t expected: Koinos had been around for a while, and yet there was no way to use it from a phone. Just browser extensions. Nothing native on mobile. It was a well-shaped gap: the problem was obvious, the scope was finite, and I’d find out quickly whether I could actually pull it off.&lt;/p&gt;
&lt;h2 id=&quot;what-konio-is-a-native-wallet-for-ios-and-android&quot;&gt;What Konio is: a native wallet for iOS and Android&lt;/h2&gt;
&lt;p&gt;I started writing Konio on 22 June, and by autumn it was already on both app stores: a native wallet for iOS and Android, built with &lt;a href=&quot;https://reactnative.dev/&quot;&gt;React Native&lt;/a&gt; and &lt;a href=&quot;https://expo.dev/&quot;&gt;Expo&lt;/a&gt;, using &lt;code&gt;koilib&lt;/code&gt; to talk to the chain.&lt;/p&gt;
&lt;p&gt;The feature list was what you’d expect. Create or import a wallet from a seed phrase, then derive additional named accounts from that same seed so there was only one thing to keep safe. Biometric unlock and an autolock timer. Send and receive tokens and NFTs, with an address book so I wasn’t pasting base58 strings by hand. &lt;a href=&quot;https://walletconnect.network/&quot;&gt;WalletConnect&lt;/a&gt;, so the wallet could sign for dApps — including scanning a QR code from a desktop browser. An in-app browser for dApps. Multiple networks. Eleven languages, because the Koinos community was scattered across a lot of countries and none of them were mine.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Konio main screen on Android: a $25.83 total balance, an account named Main with a truncated address, a MANA row at 100%, and KOIN and VHP balances&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1242&quot; height=&quot;1780&quot; src=&quot;https://adrianofoschi.com/_astro/balance-screen.CSlv1CrL_ZjGNdx.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;The main screen. Mana is the resource that makes transactions free on Koinos, so it gets a row of its own next to the balances.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Konio send screen: a recipient field, recent recipients, the user’s own accounts, and an address book with saved contacts&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1242&quot; height=&quot;1780&quot; src=&quot;https://adrianofoschi.com/_astro/send-screen.BAClPj3r_Z9SPbe.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Sending tokens. Most of the work in a wallet is in the ordinary screens, not the cryptography.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That summer Koinos ran a hackathon, the Supercharger, from 10 July to 7 September. I entered Konio and won. It’s a small ecosystem and I want to be honest about the scale of that — but it was enough to settle whether the gap I’d picked was real or just something I’d talked myself into.&lt;/p&gt;
&lt;p&gt;The cryptography, honestly, was the easy part. &lt;code&gt;koilib&lt;/code&gt; signed the transactions; I didn’t need to invent anything. The hard part was somewhere else, somewhere I didn’t expect.&lt;/p&gt;
&lt;h2 id=&quot;the-hard-part-was-state&quot;&gt;The hard part was state&lt;/h2&gt;
&lt;p&gt;A wallet had no server. There was no backend holding the truth, no account to restore from, no “log in on your new phone and everything comes back.” Whatever the app knew lived on that device and nowhere else, and a meaningful slice of it — private keys, the password — could never leave the operating system’s secure storage. Get the state layer wrong and you didn’t get a bug report, you got somebody who couldn’t reach their money.&lt;/p&gt;
&lt;p&gt;On top of that, everything in a wallet was connected to everything else. Forty-one screens all read and wrote the same handful of entities: accounts, coins, networks, contacts, NFTs, transactions, WalletConnect sessions, mana balances. Adding an account had to create the default coin entries for every configured network. Switching network had to change what half the app was showing.&lt;/p&gt;
&lt;p&gt;For state I used &lt;a href=&quot;https://hookstate.js.org/&quot;&gt;Hookstate&lt;/a&gt; — a little-known library, but a very capable one if you use it with care, lighter and more adaptable than alternatives like Zustand. The persistence layer, though, I wrote by hand.&lt;/p&gt;
&lt;p&gt;I built it around an adapter: every store saved and reloaded its data through a common interface, but the backend behind that interface changed depending on what it had to hold. Ordinary data went through the phone’s normal storage; private keys and the password went through the operating system’s encrypted keystore instead — the same mechanism that protects system passwords, not something I built myself. The code that read and wrote the two kinds of data was identical; only the adapter handed to it changed. So the difference between “ordinary data” and “data that needs protecting” wasn’t a rule scattered through the code, it was the choice of a single adapter.&lt;/p&gt;
&lt;p&gt;The other piece was hydration: on app launch, state started out empty, and data got reloaded from disk asynchronously — that took a moment. In that moment, a balance shown as zero was indistinguishable, to the user, from a balance that was actually zero: it looked exactly like a wallet that had lost their money. To avoid that, every store explicitly flagged when it had finished reloading, and screens waited on that flag before showing any number, instead of showing an empty state as if it were already the truth.&lt;/p&gt;
&lt;p&gt;Accounts, coins, and networks constantly referred back to each other, and wiring them all directly to one another would have created a tangle of dependencies. Instead, every store registered itself under a name and looked up the others only when needed, rather than depending on them directly.&lt;/p&gt;
&lt;p&gt;None of this was exotic. It was the same problem as any offline-first app: the device is the source of truth, persistence is async, and the data model is a graph rather than a list. What sharpened my thinking was the stakes. In most apps, a state bug means a wrong number on a screen and a refresh. Here, the number on the screen was the only record.&lt;/p&gt;
&lt;h2 id=&quot;getting-it-published-was-a-second-project&quot;&gt;Getting it published was a second project&lt;/h2&gt;
&lt;p&gt;Writing the wallet took a few months. Getting Apple to accept it took a different kind of stubbornness, and none of it was technical.&lt;/p&gt;
&lt;p&gt;The blocking rule sits in the &lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/&quot;&gt;App Store Review Guidelines&lt;/a&gt; at 3.1.5(b): an app may offer virtual currency storage only if it comes from a developer enrolled as an organization. An individual developer account can’t publish a wallet at all — full stop, regardless of what the app does or how well it does it. So the first hard requirement for shipping this project had nothing to do with software: I needed a legal entity, and I didn’t have one. Konio went out under &lt;a href=&quot;https://www.e-time.it/en/&quot;&gt;E-Time&lt;/a&gt;, the software house I was working for at the time, which agreed to act as the publishing entity.&lt;/p&gt;
&lt;p&gt;That got me into the queue. The queue was its own education. Crypto apps get treated as guilty until proven otherwise, and the rejections read like they’d been written without anyone actually opening the app: objections to behavior that wasn’t there, points I’d already fixed and explained in the previous submission coming back verbatim, replies that didn’t engage with the answer I’d already given. Every round cost days, and none of those days made the app any better.&lt;/p&gt;
&lt;p&gt;I don’t think that scrutiny is unreasonable in principle — a wallet is one of the few categories of app where a malicious build can drain someone’s savings, and the store is the only thing standing in front of that. But the process, as I experienced it, filtered on paperwork and pattern-matching rather than on whether the software was any good. A solo developer with no company behind them doesn’t get past the first gate, and that has nothing to do with the quality of what they built.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/konio-io/konio-mobile&quot;&gt;Konio’s source&lt;/a&gt; is on GitHub, under GPL-3.0 — publishing it felt like the right way to close out a project that a hackathon and an App Store review had already put through enough scrutiny. What stays with me, after seven months of writing it, is a habit: treating local state as a system with its own architecture — adapters, hydration, a dependency graph — rather than as a bag of variables the screens happen to share.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>Building software my school needed</title><link>https://adrianofoschi.com/blog/school-software-at-16/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/school-software-at-16/</guid><description>Everyone expected me to choose the liceo. I picked a technical institute for computer science instead — and spent the first two years doing accounting.</description><pubDate>Tue, 25 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2003, right after middle school, for someone with my grades the path seemed already decided: a liceo. A technical or industrial institute, in the mind of everyone around me, was the choice you made when you couldn’t handle something more demanding — not something a good student did.&lt;/p&gt;
&lt;p&gt;I had already made up my mind, though. A technical institute meant, sooner or later, computer science — the subject I actually cared about, after years spent tinkering with footballmatch.it. A liceo would have meant years of humanities subjects that, for what I had in mind to do, seemed close to useless. I didn’t choose the technical track because it was a good fit for someone “like me”: I chose it because it was the only place where I’d actually get to study what interested me.&lt;/p&gt;
&lt;p&gt;There was one detail I hadn’t accounted for: at that institute, computer science was only taught seriously starting in the third year. The first two years were almost entirely accounting — balance sheets, bookkeeping, the basics of business economics. I’d picked that school specifically to do computer science, and for two years I did something else entirely.&lt;/p&gt;
&lt;h2 id=&quot;scrutini-the-software-that-printed-every-report-card&quot;&gt;Scrutini: the software that printed every report card&lt;/h2&gt;
&lt;p&gt;Everything changed in the third year, in 2006. That’s when my school’s principal noticed what I could actually build, and decided to find out how far that went.&lt;/p&gt;
&lt;p&gt;The first assignment was “Scrutini,” handed to me and a close friend of mine — the same one I’d go on to share years of study, work, and life with. Scrutini was software for exactly the grading sessions where teachers finalize marks at the end of a term: teachers enter grades, the system produces the reports and prints the actual pagelle — report cards — that go home to parents. We built it in Microsoft Access. Not a glamorous choice, but the right one: a desktop database tool a small school could actually run and maintain, with forms for data entry and a report engine built in, no separate hosting or infrastructure to worry about. Behind the forms, though, there was real programming: we built the report engine in Visual Basic, which I already knew. What mattered wasn’t the technology, though — it was that two sixteen-year-olds had just been handed the software that would produce every student’s official grades. Nobody double-checked that decision for us. It just had to work.&lt;/p&gt;
&lt;h2 id=&quot;geco-double-entry-bookkeeping-in-visual-basic&quot;&gt;GeCo: double-entry bookkeeping, in Visual Basic&lt;/h2&gt;
&lt;p&gt;The second project was smaller in scope but had more personality: “GeCo,” short for Gestione Contabilità — accounting management — with a gecko as its icon because the name asked for it. And this is where those two years of accounting, which had felt like wasted time back then, suddenly turned useful: the course included business economics, and a good chunk of those hours were spent on partita doppia — double-entry bookkeeping exercises. GeCo was a Visual Basic tool for exactly that: generating and checking double-entry exercises.&lt;/p&gt;
&lt;p&gt;There’s something I didn’t clock at the time: I was building the practice tool for the exact subject I was being taught, in parallel with actually learning it. That loop — learn the domain, then build the tool that teaches it — turned out to be one I’d repeat many times since.&lt;/p&gt;
&lt;h2 id=&quot;ambito5-a-portal-five-municipalities-ran-on&quot;&gt;Ambito5: a portal five municipalities ran on&lt;/h2&gt;
&lt;p&gt;The bigger jump came with Ambito5: a real institutional project, a web portal supporting a formal social-services collaboration between five municipalities — the kind of inter-comune administrative arrangement Italian public services run on, coordinating things like social assistance across towns too small to each staff their own office. This wasn’t a school exercise anymore; it was public infrastructure, and it earned us a scholarship for building it.&lt;/p&gt;
&lt;p&gt;It was also our first real requirements-and-architecture job, not just implementation. Five municipalities meant five sets of needs, and someone had to turn that into one coherent system before a single line of code got written. We built it on .NET, with AJAX for the interactive parts — which in those years was still new enough that using it was a deliberate, slightly bleeding-edge choice, not a default.&lt;/p&gt;
&lt;h2 id=&quot;what-it-taught-me&quot;&gt;What it taught me&lt;/h2&gt;
&lt;p&gt;By the end of those years the pattern was already set: whenever someone handed me a real problem, I’d go build the actual thing rather than just study it. Software a real institution depends on doesn’t come with training wheels — nobody double-checks it for you just because you’re young. Trust gets earned by the work holding up, nothing else.&lt;/p&gt;
&lt;p&gt;In 2010, a couple of years after graduating, I went back to that same school — this time as a teacher of programming and web design. First a student, then a teacher.&lt;/p&gt;
</content:encoded><category>early-projects</category></item><item><title>I didn&apos;t want to trade crypto. I wanted to build on it.</title><link>https://adrianofoschi.com/blog/how-i-found-koinos/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/how-i-found-koinos/</guid><description>In 2020 I bought crypto like everyone else. By the time I found DeFi, I realized what I actually wanted wasn&apos;t to trade it — it was to build on it, and Solidity&apos;s learning curve sent me looking for something else.</description><pubDate>Sun, 14 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2020 I got into cryptocurrencies, not blockchain in any technical sense yet. It was a world I’d always wanted to explore, but for years it had stayed in the background — something I heard about without ever really stepping in. I started the way most people do: buying small amounts on one of the big centralized exchanges, more out of curiosity than conviction. It was there, browsing through the assets on offer, that I kept running into a name that came up more and more often: DeFi.&lt;/p&gt;
&lt;p&gt;DeFi pulled me in almost immediately, and not just for the yields. There was an idea underneath it that stuck with me: building freely, without permission, without an intermediary deciding what could or couldn’t be done. A protocol, a contract, rules written into code — and anyone could use it, or build something else on top of it. 2021 made that idea feel almost tangible: it was the year of the bull run, and with it an explosion of projects that seemed to appear every week — aggressive new DeFi protocols, the NFT boom, the first play-to-earn games, the wave (often more promise than substance) of the metaverse. Not everything born in those months made sense, but the pace at which it was born did: something was clearly being built, and anyone who wanted in could get in.&lt;/p&gt;
&lt;p&gt;There was something familiar in all of this, even if I didn’t quite see it at the time. In 2001, still a kid, I’d gone from playing a video game to building tools that modified it — from consumer to someone building what other consumers would use. Twenty years later it was happening to me again, just with a different object in my hands: I’d entered the crypto world as a small investor, someone who buys and watches, and I was realizing that the part I actually cared about wasn’t buying — it was understanding how these things get built.&lt;/p&gt;
&lt;h2 id=&quot;what-does-it-take-to-build-on-a-blockchain&quot;&gt;What does it take to build on a blockchain?&lt;/h2&gt;
&lt;p&gt;So I started studying what it actually meant to build on a blockchain. It was complex, but less than reverse-engineering a video game with no documentation and no organized community to help — here at least there was documentation, fragmented as it often was. I learned the basics: what a blockchain is, what a smart contract is, how a piece of code can live on a distributed network and enforce rules that nobody can unilaterally change once published. Things that forced me to unlearn a few habits from traditional web development, more than I expected going in.&lt;/p&gt;
&lt;h2 id=&quot;why-solidity-is-hard-to-approach-from-web2&quot;&gt;Why Solidity is hard to approach from web2&lt;/h2&gt;
&lt;p&gt;But the moment I actually tried to get hands-on, I ran into an obstacle I hadn’t expected: Solidity, the language most smart contracts are written in, is not easy to approach coming from web2. It’s not just the syntax. It’s a different way of thinking: every operation has a computational cost you have to calculate and manage — gas, the unit of measure of the Ethereum Virtual Machine — and around that model sit rigid, very specific concepts that a JavaScript developer simply never had to consider. I realized this wasn’t just my own personal struggle: it was a real barrier to entry, one keeping an entire generation of web2 developers, used to a completely different paradigm, out of web3.&lt;/p&gt;
&lt;h2 id=&quot;finding-koinos-no-ico-no-pre-mine&quot;&gt;Finding Koinos: no ICO, no pre-mine&lt;/h2&gt;
&lt;p&gt;It was while looking for an alternative that I found Koinos, and it struck me right away as an ethically flawless project. No ICO, no pre-mine: the founding team — coming off an earlier project in another blockchain, which they’d left after a hostile attack on the network — had chosen to build by working for equity, without raising outside capital. A philosophy that felt close to Bitcoin’s original one: using available computational resources without needing dedicated hardware, with a Proof-of-Burn consensus designed to combine the economics of proof-of-work with the efficiency of proof-of-stake. And then there was governance: protocol upgrades happen through a network vote, not a hard fork — Koinos calls itself the most upgradeable blockchain there is, and the fact that it could evolve without splitting in two won me over.&lt;/p&gt;
&lt;h2 id=&quot;the-stack-that-made-me-stay&quot;&gt;The stack that made me stay&lt;/h2&gt;
&lt;p&gt;But what actually made me fall for it was the application stack: smart contracts are written in AssemblyScript, a syntax very close to TypeScript, familiar as I already was with JavaScript. The APIs are REST, not the usual opaque RPC endpoints of the blockchain world, and &lt;code&gt;koilib&lt;/code&gt; made client-side integration as immediate as working with any other web API. Even the fee model followed the same philosophy of accessibility: no gas to pay on every transaction, but “mana,” a resource that regenerates and that a “payer” can even front on the end user’s behalf. Koinos wasn’t just solving a technical problem — it was deliberately lowering the exact barrier I’d just run into, giving web2 developers immediate access to web3.&lt;/p&gt;
&lt;p&gt;I hadn’t written a single line of code on Koinos yet, but I already knew that was the door I’d been looking for. Not the most famous project, not the one with the biggest community or the loudest marketing — simply the one that, out of everything I’d looked at, seemed genuinely built to let people like me in. Sometimes a technology choice isn’t just about specs: it’s recognizing a philosophy you see yourself in.&lt;/p&gt;
</content:encoded><category>blockchain</category></item><item><title>Building was always the point</title><link>https://adrianofoschi.com/blog/fifa-modding-at-14/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/fifa-modding-at-14/</guid><description>From LEGO ziggurats and a 1998 Pentium to hex-editing FIFA files by hand: founding footballmatch.it, building a suite of import tools, and landing a Superpatch on an Italian magazine&apos;s cover.</description><pubDate>Tue, 18 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;building-not-the-result&quot;&gt;Building, not the result&lt;/h2&gt;
&lt;p&gt;As a kid I didn’t have many toys, and I was a quiet, reflective sort who mostly kept to himself. LEGO was the only toy that mattered. I was fascinated by ancient history, and I’d spend hours flipping through history atlases, staring at ziggurats and pyramids, then rebuilding them piece by piece with thousands of bricks. When I finished — hours later — I’d show my parents, and tear it down a few minutes after. It wasn’t frustration: I felt satisfied, but the result itself barely interested me. What I actually loved was building.&lt;/p&gt;
&lt;h2 id=&quot;a-pentium-in-the-house-in-1998&quot;&gt;A Pentium in the house, in 1998&lt;/h2&gt;
&lt;p&gt;In 1998, at nine years old, an encyclopedia on CD-ROM arrived in the mail. Floppy disks were still the norm back then, and a personal computer at home was far from a given — especially in Italy. My father looked at the box and asked the right question: “How do we even look at this? Should we get a computer?” The answer was a Pentium 1, one of the processors that were bringing computers into ordinary households in those years, with plenty of successors still to come over the next decade. I didn’t know it yet, but that machine would become my first real tool.&lt;/p&gt;
&lt;h2 id=&quot;fifa-arrives-and-leaves-just-as-fast&quot;&gt;FIFA arrives, and leaves just as fast&lt;/h2&gt;
&lt;p&gt;FIFA ’98 showed up for my birthday. Playing it was exciting — FIFA was becoming the PC football game of the era — but the excitement didn’t last: I got bored fast. The following year I got FIFA ’99, and this time the excitement never even arrived. The internet, meanwhile, was a different world: a painfully slow 56k connection, little content, hard to find, and — in Italy at least — used by a small minority of the initiated.&lt;/p&gt;
&lt;h2 id=&quot;the-communities-behind-the-game&quot;&gt;The communities behind the game&lt;/h2&gt;
&lt;p&gt;I discovered that behind FIFA there were huge online communities, including actual developers. They built patches that updated the original game: team names and lineups after every transfer window, more realistic kits and boots, player faces, new stadiums, and much more. The communities I remember most fondly are Fifamania.it, Fifaonline.it, and Soccergaming.com. I started as a user, downloading patches and installing them, but that wasn’t enough for long — I wanted to understand how they worked. What were those &lt;code&gt;.bin&lt;/code&gt; files? How had people made them?&lt;/p&gt;
&lt;h2 id=&quot;learning-alone-one-byte-at-a-time&quot;&gt;Learning alone, one byte at a time&lt;/h2&gt;
&lt;p&gt;There was no Stack Overflow, let alone ChatGPT. Information was scarce, and whoever had it tended to guard it jealously instead of sharing it. I taught myself, the hard way, what those files contained and how to modify them — one hex editor session at a time, with nobody around to tell me where to start.&lt;/p&gt;
&lt;h2 id=&quot;fifa-2000-adsl-the-first-editors&quot;&gt;FIFA 2000, ADSL, the first editors&lt;/h2&gt;
&lt;p&gt;FIFA 2000 arrived along with ADSL, and everything sped up. The internet became accessible to everyone, and information started circulating far more freely. I finally managed to build my first patches using editors that EA Sports itself provided. I remember endless restarts of the game: every patch I broke by mistake would crash everything, and I’d start over. I began producing kits, boots, and player faces, published on the same sites I’d discovered as a user. The happiness was overwhelming: my name was online, and thousands of people were using and appreciating my work.&lt;/p&gt;
&lt;h2 id=&quot;footballmatchit-the-step-up&quot;&gt;footballmatch.it, the step up&lt;/h2&gt;
&lt;p&gt;In 2001 I took the leap: I built my own site, a competitor to the ones I’d been using as a fan — footballmatch.it. I put it together in one sleepless night with my father, using Microsoft FrontPage. At first I did everything with the visual editor, driving myself crazy trying to make it look the same in Internet Explorer and in Mozilla. Then I discovered that underneath that visual editor was plain HTML and CSS, and that writing it by hand was far more fun — and far more reliable — than trusting the WYSIWYG. That was the moment, without my quite realizing it yet, that sealed a pact with my future: I had become a programmer. I put together a team, we shipped free updates every week, and I reached 10,000 unique monthly visitors — a huge number for the era, and even more so for a teenager. Across the community, thousands of people used my work assuming they were dealing with a grown man, maybe with a family of his own. I was just a kid.&lt;/p&gt;
&lt;h2 id=&quot;the-tools-before-the-product&quot;&gt;The tools before the product&lt;/h2&gt;
&lt;p&gt;Redoing every texture and every record by hand, in a hex editor, didn’t scale to what we needed. So I started building tools — not as an end in themselves, but because kits, boots, faces and flags all had to be produced at volume for the patches we were actually shipping. A few of the tools that came out of that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Footballmatch Face Importer 2.0&lt;/strong&gt; — imported custom player face textures into the game’s &lt;code&gt;.fsh&lt;/code&gt; format, with a live 3D preview of the face and hair mesh before importing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Footballmatch Player Edit&lt;/strong&gt; — a fuller editor: face textures, hairstyles, boots, gloves, even shin guards, all through a proper Windows GUI instead of a hex editor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Footballmatch Flags Importer&lt;/strong&gt; — swapped team badges and banners, with thumbnail previews of every asset before you committed to importing it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Footballmatch Shoes Importer&lt;/strong&gt; — the same idea, scoped to boot textures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;Footballmatch Face Importer 2.0: two panes previewing a player’s head and hair mesh, dropdowns for the structure and the hairstyle, and a list of face texture files with import buttons&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;699&quot; height=&quot;550&quot; src=&quot;https://adrianofoschi.com/_astro/face-importer.BNVcDR2m_Z48MCj.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Footballmatch Face Importer 2.0 — the preview was the whole point: see the head before writing it into the game’s texture archive.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;None of these tools, though, were really the product: they were the assembly line. The real product was the patch — a complete, installable bundle of updated squads, kits, boots, flags, faces and eventually stadiums, turning a stale FIFA install into something close to the current season.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Footballmatch Player Edit, a Windows tool for editing FIFA player textures, boots and gloves&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;428&quot; height=&quot;431&quot; src=&quot;https://adrianofoschi.com/_astro/player-edit.CqSDr9FE_Z2nGGgH.webp&quot; srcset=&quot;&quot;&gt;
&lt;em&gt;Footballmatch Player Edit — face, hair, boots, gloves and shin guards, all through dropdowns instead of a hex editor.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-superpatch-on-the-newsstand&quot;&gt;The Superpatch on the newsstand&lt;/h2&gt;
&lt;p&gt;I reached out to &lt;em&gt;Giochi per il mio Computer&lt;/em&gt;, one of the best-known PC gaming magazines in Italy at the time, to pitch the Superpatch — the full package that updated FIFA with squads, kits, flags and everything else. It made the newsstand, and millions of Italians saw my name printed on the cover. I was stunned: up to that point, my audience had been a handful of forums. Seeing it land in front of an entire newsstand’s worth of readers was the first time the hobby felt like it might be more than a hobby.&lt;/p&gt;
&lt;h2 id=&quot;building-not-playing&quot;&gt;Building, not playing&lt;/h2&gt;
&lt;p&gt;In the meantime, without quite noticing it, I’d become an all-round editor: 2D graphics for kits and boots, 3D graphics for stadiums and faces, plus the tools to produce all of it at scale. And yet, of all those sleepless nights, not one was spent actually playing a match of FIFA or PES. My mission had never been to play — it was to build. It was that realization, pieced together without my knowing it, that led me — a few years later — to choose my course of study: an experimental computer science program.&lt;/p&gt;
</content:encoded><category>early-projects</category></item><item><title>My space, my challenges, my memories</title><link>https://adrianofoschi.com/blog/my-space-my-challenges-my-memories/</link><guid isPermaLink="true">https://adrianofoschi.com/blog/my-space-my-challenges-my-memories/</guid><description>I build, I experiment, I learn — and from here on I&apos;m writing it down, instead of leaving it in my head or in a conversation I had once.</description><pubDate>Tue, 21 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’m someone who needs to build. I like experimenting, getting my hands on things I don’t know, learning something new and seeing how far I can take it — and that’s as true of my work as it is of what I do when nobody asks me to.&lt;/p&gt;
&lt;p&gt;What’s been missing is keeping any record of it. The experience stays in my head, or I tell someone about it once, out loud, and that’s where it stops. From here on I’m going to write it down.&lt;/p&gt;
&lt;p&gt;Not out of nostalgia. Writing it is useful: to me, because it puts what I’ve done in order and makes it something I can show; to whoever is up against the same problem and is looking for someone who has already been through it; and to anyone following along who wants to know what I’m working on.&lt;/p&gt;
&lt;h2 id=&quot;how-i-got-here&quot;&gt;How I got here&lt;/h2&gt;
&lt;p&gt;I taught myself to program long before anyone taught me: taking things apart to see how they were made inside, and putting them back together different. When it came time to choose a school I chose it for that, against the advice of almost everyone around me, and I arrived at university already knowing what I wanted to do with it.&lt;/p&gt;
&lt;p&gt;Then it became the job: real clients, other people’s projects, companies, products that keep going for years. Every stage taught me something the one before it couldn’t. Working for someone who pays you isn’t working for yourself, and maintaining something for years isn’t building it.&lt;/p&gt;
&lt;p&gt;What never changed is the other half: what I build when nobody asks me to.&lt;/p&gt;
&lt;h2 id=&quot;what-i-build-when-nobody-asks-me-to&quot;&gt;What I build when nobody asks me to&lt;/h2&gt;
&lt;p&gt;There are always two or three things open, outside working hours. Sometimes they come from a problem of my own that nobody has solved the way I’d want; more often from a technology I want to genuinely understand, and the only way I know to understand one is to build something with it that has to work.&lt;/p&gt;
&lt;p&gt;That’s where you actually learn, because that’s where choices have a price. Documentation tells you what a tool does; building something real on top of it tells you what it doesn’t do, and what it costs you to find that out late.&lt;/p&gt;
&lt;p&gt;Not everything gets anywhere. Some things end up in people’s hands and get used, some stop before that, and some I drop because I’ve learned what I wanted to learn and the thing itself is no longer any use to me. I don’t call those failures: I got what I came for anyway.&lt;/p&gt;
&lt;h2 id=&quot;what-youll-find-here&quot;&gt;What you’ll find here&lt;/h2&gt;
&lt;p&gt;Accounts of things I actually built, with the technical detail that matters and the results as they went, including the ones that don’t flatter me. A project that didn’t work, explained well, is worth more than a successful one told halfway.&lt;/p&gt;
&lt;p&gt;These aren’t tutorials: I’m not trying to teach anyone how to use a tool, and there are better places for that. There’s no thesis to prove either. The subjects will change, because what I work on changes; what stays the same is the angle — a thing that got built, why I built it that way, and what I got out of it.&lt;/p&gt;
&lt;p&gt;This is the space to put them in. Starting here.&lt;/p&gt;
</content:encoded><category>career</category></item></channel></rss>