<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Saad Ali Malik — Writing</title><description>Articles by Saad Ali Malik, full-stack engineer.</description><link>https://saadalimalik.com</link><language>en</language><item><title>I use AI to write code. Here is how I keep the codebase mine.</title><link>https://saadalimalik.com/blog/ai-writes-code-i-keep-the-codebase</link><guid isPermaLink="true">https://saadalimalik.com/blog/ai-writes-code-i-keep-the-codebase</guid><description>AI coding tools made me faster and made a lot of codebases worse. The difference is a few habits that cost me an hour a day and save me weeks.</description><pubDate>Tue, 22 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I use AI coding tools every day, including to build the site you are reading. I also spend a good part of my working life inside codebases that were built with the same tools by someone who did not read the output — and those two facts are not a contradiction. They are the whole point.&lt;/p&gt;
&lt;p&gt;Used well, these tools compress the time between an idea and a working product from weeks to days. Used carelessly, they produce something that works on Tuesday and cannot be changed by Friday, because nobody understands it and nothing is tested. This is what I actually do to stay on the right side of that line. None of it is clever. All of it is discipline.&lt;/p&gt;
&lt;h2 id=&quot;i-read-every-line-that-ships&quot;&gt;I read every line that ships&lt;/h2&gt;
&lt;p&gt;This is the rule everything else hangs off, so I’ll say it plainly: nothing generated goes into a commit without me reading it as carefully as I would read a pull request from a new teammate.&lt;/p&gt;
&lt;p&gt;Not skimming. Reading. Asking, for each block, “why is this here, and what happens when it is wrong?” The tools are good enough now that the code &lt;em&gt;looks&lt;/em&gt; right almost every time, which is precisely why reading matters more than it used to. The bugs are no longer syntax errors you’d catch in a glance; they’re a retry loop with no limit, a check that runs before the rate limit instead of after, a fallback that fails open when it should fail closed.&lt;/p&gt;
&lt;p&gt;Reading costs me maybe an hour a day. I have never once regretted it, and I have on several occasions found the kind of issue that would have become a 3 a.m. phone call.&lt;/p&gt;
&lt;h2 id=&quot;small-diffs-one-intent-each&quot;&gt;Small diffs, one intent each&lt;/h2&gt;
&lt;p&gt;The failure mode I see most often is “make me a user system” as a single prompt, followed by a two-thousand-line diff that touches twelve files. Even if it works, nobody can review it — including the person who asked for it.&lt;/p&gt;
&lt;p&gt;I work the way I would with a junior engineer I trust but supervise: one intent at a time. “Add the migration for the likes table.” Read it. “Now the endpoint, POST only, same-origin check like the contact form.” Read it. “Now the button.” Each step is small enough to hold in my head, which means each step is small enough to be &lt;em&gt;wrong in an obvious way&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It feels slower. It is faster, because I never spend a day untangling a change I don’t understand.&lt;/p&gt;
&lt;h2 id=&quot;tests-are-the-contract-not-the-afterthought&quot;&gt;Tests are the contract, not the afterthought&lt;/h2&gt;
&lt;p&gt;If the tool writes the code and I write nothing, I have no independent statement of what the code is supposed to do. So for anything with logic — validation, money, security, a parser — I decide what the test is &lt;em&gt;before&lt;/em&gt; the code exists, and the code has to pass it.&lt;/p&gt;
&lt;p&gt;The test does not have to be big. A single &lt;code&gt;test_*&lt;/code&gt; file that fails if the logic breaks is enough. What matters is that it was written from my understanding of the requirement, not derived from the implementation, so it catches the case where the model and I disagree about what “done” means.&lt;/p&gt;
&lt;p&gt;For this site, that looks like: a unit test for every validation function, an end-to-end test that fills in the contact form with a disposable email and expects a specific error, and an accessibility check in both themes on every page. When a generated change breaks one of those, the tool did not “make a mistake”; the contract did its job.&lt;/p&gt;
&lt;h2 id=&quot;conventions-the-tool-cant-drift-from&quot;&gt;Conventions the tool can’t drift from&lt;/h2&gt;
&lt;p&gt;Generated code drifts. Ask for the same thing on three different days and you’ll get three styles, three naming schemes, three ways to handle errors. Left alone, a codebase built this way becomes a museum of the tool’s moods.&lt;/p&gt;
&lt;p&gt;The fix is boring and effective: make the conventions mechanical. A formatter that rewrites everything on save. A linter with the rules I actually care about, failing the build. A short document that says how a page is structured, where content lives, what an endpoint looks like — written once and pointed at every time. The same rules a human team needs, applied more strictly, because the “team” has no memory between sessions.&lt;/p&gt;
&lt;p&gt;On this site every colour is a named token, every endpoint follows the same pattern as the first one, and every page is built from the same handful of primitives. Not because generated code can’t vary, but because I chose not to let it.&lt;/p&gt;
&lt;h2 id=&quot;understanding-first-then-speed&quot;&gt;Understanding first, then speed&lt;/h2&gt;
&lt;p&gt;The tools are extraordinary at producing a plausible solution to a problem they have not fully understood. So am I, on a bad day. The difference is that I can notice.&lt;/p&gt;
&lt;p&gt;Before I ask for a change, I trace the real flow myself: which files it touches, what calls what, where the data comes from. Then I ask. The shortest diff in the wrong place is not lazy; it’s a second bug. Comprehension is the one part of the job I never delegate.&lt;/p&gt;
&lt;h2 id=&quot;when-i-dont-use-it&quot;&gt;When I don’t use it&lt;/h2&gt;
&lt;p&gt;Some things I still write by hand, or at least design by hand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data models and migrations.&lt;/strong&gt; These decisions outlive every line of application code. I want to have made them on purpose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security boundaries.&lt;/strong&gt; Authentication, authorisation, anything that touches a secret or a payment. I’ll accept help, but the shape is mine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anything I can’t evaluate.&lt;/strong&gt; If I couldn’t tell a good answer from a bad one, I have no business shipping the tool’s answer. I go and learn first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-result&quot;&gt;The result&lt;/h2&gt;
&lt;p&gt;Working this way, I get most of the speed and none of the “we can’t touch that module” dread. Products go from a first sketch to production in weeks, and a year later they can still be changed by whoever inherits them — which, on a small team, is usually me.&lt;/p&gt;
&lt;p&gt;If you’re weighing up whether AI-built software is something you can bet a business on: it is, with the habits above, and it isn’t without them. The tool is not the risk. Unread code is.&lt;/p&gt;
&lt;p&gt;I’m happy to talk about how this applies to what you’re building — &lt;a href=&quot;/contact&quot;&gt;start a conversation here&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>ai</category><category>engineering</category><category>process</category></item><item><title>What makes a knowledge assistant useful to a small business</title><link>https://saadalimalik.com/blog/what-makes-a-knowledge-assistant-useful</link><guid isPermaLink="true">https://saadalimalik.com/blog/what-makes-a-knowledge-assistant-useful</guid><description>It is not the model. Three decisions matter more: sources you can see, the ability to act, and a log of what people actually ask.</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every few weeks someone shows me a chatbot they built over their company’s documents, and it works — in the demo. Ask it a question from the onboarding PDF and it answers in a confident paragraph. Then two months later I ask how it’s going and the answer is “people stopped using it”.&lt;/p&gt;
&lt;p&gt;I have built a few of these now, for businesses with a dozen to a couple of hundred staff, and the ones that stuck had almost nothing to do with which model was underneath. They came down to three product decisions that are easy to skip because the demo works without them.&lt;/p&gt;
&lt;h2 id=&quot;1-every-answer-shows-where-it-came-from&quot;&gt;1. Every answer shows where it came from&lt;/h2&gt;
&lt;p&gt;A retrieval-based assistant (the “RAG” pattern: find the relevant passages, hand them to the model, have it answer from those) is only as trustworthy as its worst answer. The first time it tells someone the wrong holiday policy with total confidence, that person is done. And they will tell everyone else.&lt;/p&gt;
&lt;p&gt;The fix is not a better model. It is showing the source, every time, right under the answer: the document name, the section, a link that opens it at the right place. Two things happen when you do this.&lt;/p&gt;
&lt;p&gt;First, people check. In the beginning they check everything, which is exactly what you want — it is how trust gets built, one verified answer at a time. Later they check only the ones that surprise them.&lt;/p&gt;
&lt;p&gt;Second, the assistant becomes a search engine that can talk. Half the value of the tool in the businesses I worked with was not the generated paragraph; it was that the paragraph pointed at the &lt;em&gt;right document&lt;/em&gt;, which nobody could find before. A hint of what the model thinks plus a reliable link beats a beautiful answer with no provenance.&lt;/p&gt;
&lt;p&gt;Practically this means the retrieval layer has to keep metadata — file, heading, page — alongside every chunk, and the prompt has to require the model to cite chunk identifiers that you then turn into links. It is not much code. It is the difference between a toy and a tool.&lt;/p&gt;
&lt;h2 id=&quot;2-it-can-do-something-not-just-say-something&quot;&gt;2. It can do something, not just say something&lt;/h2&gt;
&lt;p&gt;The second decision is whether the assistant can &lt;em&gt;act&lt;/em&gt;. A knowledge assistant that only answers questions is a nicer manual. One that can also update a record is a colleague.&lt;/p&gt;
&lt;p&gt;For one services business, the most-used feature was not question answering at all. It was “mark job 4821 as complete and note that the customer asked for a Tuesday follow-up” — typed in plain language, turned into two writes against the tools staff already used. The assistant read from the same database it answered questions from, so it could confirm what it was about to change before changing it.&lt;/p&gt;
&lt;p&gt;This is where the “agent” word earns its keep, and where the engineering gets real:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every action goes through a function the assistant is allowed to call, with typed arguments, so it cannot invent a new capability.&lt;/li&gt;
&lt;li&gt;Anything destructive asks first and shows exactly what will change.&lt;/li&gt;
&lt;li&gt;Every action is logged with who asked and what the model saw, so a mistake can be traced and reversed.&lt;/li&gt;
&lt;li&gt;The model never holds credentials. It asks the backend to do things on behalf of a logged-in person, with that person’s permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do that and staff stop thinking of it as “the chatbot” and start thinking of it as the fastest way to get things done. Skip it and the assistant stays in the “nice, I guess” category forever.&lt;/p&gt;
&lt;h2 id=&quot;3-log-the-questions--that-is-the-real-product&quot;&gt;3. Log the questions — that is the real product&lt;/h2&gt;
&lt;p&gt;The third decision costs almost nothing and turns out to be the most valuable: keep a log of every question, whether it was answered well, and which sources were used. Then put a dashboard on it.&lt;/p&gt;
&lt;p&gt;For the business owner, that dashboard is a map of confusion. The questions people ask most are the procedures nobody can find. The questions the assistant cannot answer are the procedures that were never written down. The questions where people click through to the source and then keep reading are the documents that are out of date — the answer was technically right and practically useless.&lt;/p&gt;
&lt;p&gt;In one company, the top question for a month was a variation of “how do I get the van key on a weekend”. There was no document. There was a WhatsApp group. Writing a one-paragraph procedure removed the question entirely, and the owner realised that the assistant’s most useful output was not answers but a prioritised list of what to write next.&lt;/p&gt;
&lt;p&gt;That is the part I would build first if I had to choose. A retrieval assistant with no question log is a black box that is either working or not, and you will only find out when usage drops. With the log, the assistant tells you how to improve the assistant.&lt;/p&gt;
&lt;h2 id=&quot;what-about-the-model&quot;&gt;What about the model?&lt;/h2&gt;
&lt;p&gt;It matters less than you’d think, as long as it is decent at following instructions and staying inside the sources you give it. I have shipped these on hosted frontier models and on smaller open models running cheaply, and users noticed the three decisions above far more than the difference between models.&lt;/p&gt;
&lt;p&gt;Where the model choice &lt;em&gt;does&lt;/em&gt; matter is cost, and the answer there is boring: count tokens, set a budget per user or team, and show people their usage. A business that can see the meter trusts the tool. A surprise invoice ends the project.&lt;/p&gt;
&lt;h2 id=&quot;the-short-version&quot;&gt;The short version&lt;/h2&gt;
&lt;p&gt;If you are thinking about building one of these for your own business:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Show sources under every answer, with links.&lt;/li&gt;
&lt;li&gt;Let it take a handful of well-guarded actions in the tools you already use.&lt;/li&gt;
&lt;li&gt;Log every question and look at the log weekly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Get those three right and a modest model on a modest budget will be something your team uses every day. Get them wrong and the best model in the world is a demo that dies in two months.&lt;/p&gt;
&lt;p&gt;I build these for businesses that want their own data to work harder. If that sounds like yours, &lt;a href=&quot;/contact&quot;&gt;tell me what you have&lt;/a&gt; and I’ll tell you honestly whether an assistant is the right first step.&lt;/p&gt;</content:encoded><category>ai</category><category>rag</category><category>small-business</category></item><item><title>Three ways to run a live preview in the browser</title><link>https://saadalimalik.com/blog/live-preview-in-the-browser</link><guid isPermaLink="true">https://saadalimalik.com/blog/live-preview-in-the-browser</guid><description>What I learned testing Coolify, WebContainers and Nodepod for Summon.dev, and how to pick between them for your own product.</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When an AI writes an app for you, the moment that matters is not the code appearing. It is the moment you click a button in your own app and it works. Everything before that is a promise; that click is proof.&lt;/p&gt;
&lt;p&gt;On &lt;a href=&quot;https://summon.dev&quot;&gt;Summon.dev&lt;/a&gt; I own that moment. A user describes a product, the model builds it, and within seconds they need to see it running in a browser tab — then a developer they summon needs to open a terminal and an editor against the same running app. Getting from “the code exists” to “the app is running in front of you” turned out to be the hardest infrastructure decision in the product, so I tested three very different approaches before we committed. This is what each one is actually good at.&lt;/p&gt;
&lt;h2 id=&quot;the-job-to-be-done&quot;&gt;The job to be done&lt;/h2&gt;
&lt;p&gt;Before comparing tools it helps to be precise about what a preview has to do, because the three options fail in different places:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start fast.&lt;/strong&gt; A user who just typed an idea will wait a few seconds. They will not wait for a container to be scheduled.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run real code.&lt;/strong&gt; Node servers, package installs, databases, environment variables. A sandbox that only renders static HTML is a demo, not a preview.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Survive edits.&lt;/strong&gt; The AI regenerates files constantly. The preview has to pick up changes without a cold start each time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let a human in.&lt;/strong&gt; On Summon.dev a developer has to be able to open a shell on the same instance the user is looking at.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost nothing while idle.&lt;/strong&gt; Most previews are looked at for two minutes and abandoned.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nobody hits all five. The question is which ones you can live without.&lt;/p&gt;
&lt;h2 id=&quot;option-one-a-real-server-per-preview-coolify&quot;&gt;Option one: a real server per preview (Coolify)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://coolify.io&quot;&gt;Coolify&lt;/a&gt; is a self-hosted platform that deploys apps from a git repository or a Dockerfile onto your own servers, with a proper API. The mental model is simple: every preview is a real deployment, with its own container, its own URL and its own logs.&lt;/p&gt;
&lt;p&gt;What that buys you is fidelity. Whatever the user’s app needs — a Postgres database, a background worker, a Python service next to the Node one — it runs the way it will run in production, because it &lt;em&gt;is&lt;/em&gt; production infrastructure. A developer can &lt;code&gt;docker exec&lt;/code&gt; into it and nothing is fake. When the user is finally ready to ship, the preview and the deployment are the same artefact.&lt;/p&gt;
&lt;p&gt;What it costs you is the first thirty seconds. Building an image, pulling dependencies and scheduling a container is measured in tens of seconds at best, and every regeneration of the app risks another build. You can mitigate that with warm pools and layer caching, and we did, but you are fighting the tool’s nature. Coolify wants to deploy things; you want to &lt;em&gt;poke&lt;/em&gt; at things.&lt;/p&gt;
&lt;p&gt;There is also a bill that scales with curiosity. Idle previews hold memory. If a thousand people try your product on launch day, you have a thousand containers to reap, and the reaper is code you now own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reach for it when&lt;/strong&gt; fidelity matters more than the first ten seconds — for example, when the preview &lt;em&gt;is&lt;/em&gt; staging.&lt;/p&gt;
&lt;h2 id=&quot;option-two-run-node-inside-the-visitors-browser-webcontainers&quot;&gt;Option two: run Node inside the visitor’s browser (WebContainers)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://webcontainers.io&quot;&gt;WebContainers&lt;/a&gt; take the opposite bet: the Node runtime, the file system and the package manager all run inside the browser tab, in WebAssembly. There is no server. The user’s laptop is the preview environment.&lt;/p&gt;
&lt;p&gt;The first time you see &lt;code&gt;npm install&lt;/code&gt; complete inside a browser tab, without a network round-trip to anything you operate, it is hard not to grin. Startup is nearly instant. Edits are instant, because the AI’s output is written straight into the in-browser file system. Cost per preview is zero, and it scales to any number of users because each one brings their own compute.&lt;/p&gt;
&lt;p&gt;The limits show up exactly where the browser’s limits are. The app is sandboxed to what the browser can do: it can serve HTTP to itself, but it cannot open arbitrary sockets, so a real Postgres is out and you end up with SQLite-in-memory shims. Anything native — image processing, &lt;code&gt;sharp&lt;/code&gt;, Python — does not exist. Memory is whatever the tab is allowed. And “let a human in” gets strange: the developer’s terminal is talking to a process on the &lt;em&gt;user’s&lt;/em&gt; machine, so collaboration means syncing two browsers rather than sharing a server.&lt;/p&gt;
&lt;p&gt;There is a licensing consideration too: commercial use needs a commercial agreement, which is fair, but it belongs in the spreadsheet next to the server costs it removes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reach for it when&lt;/strong&gt; the apps you preview are mostly frontend-plus-API, the audience could be large, and the first three seconds decide whether people stay.&lt;/p&gt;
&lt;h2 id=&quot;option-three-pods-on-demand-nodepod&quot;&gt;Option three: pods on demand (Nodepod)&lt;/h2&gt;
&lt;p&gt;The third approach sits between the two: a lightweight, purpose-built environment per preview that starts in a second or two, runs real Node with a real file system, and is thrown away when the tab closes. Nodepod is one implementation of this idea; the pattern is what matters — you will find it under several names.&lt;/p&gt;
&lt;p&gt;The appeal is that it takes the good half of each of the other options. It is a real process on a real machine, so a database and a shell are normal. It is not a full deployment, so startup is fast and idle cost is small. Regenerated files are written into a running environment rather than rebuilt into an image.&lt;/p&gt;
&lt;p&gt;The catch is that the middle of the road has less signage. You are relying on a younger, more specialised tool for the part of your product that has to work every time, and when it misbehaves the community you can ask is small. The isolation guarantees are also weaker than a full container per user, which is fine for previews and not fine for anything that touches other customers’ data — so you have to be honest about which one you are building.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reach for it when&lt;/strong&gt; you need a real process and a shell, but a full deployment per preview is too slow or too expensive.&lt;/p&gt;
&lt;h2 id=&quot;how-i-would-decide-today&quot;&gt;How I would decide today&lt;/h2&gt;
&lt;p&gt;Line the five jobs up against the three options and the pattern is clear:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Coolify&lt;/th&gt;
&lt;th&gt;WebContainers&lt;/th&gt;
&lt;th&gt;Nodepod-style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starts fast&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs real code&lt;/td&gt;
&lt;td&gt;✓✓&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives edits&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;✓✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lets a human in&lt;/td&gt;
&lt;td&gt;✓✓&lt;/td&gt;
&lt;td&gt;awkward&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free while idle&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The mistake I see teams make is picking on fidelity alone, because it is the easiest thing to demo to yourself. Fidelity is what &lt;em&gt;you&lt;/em&gt; notice. Speed is what the user notices. If the preview takes twenty seconds, most people never find out how faithful it was.&lt;/p&gt;
&lt;p&gt;The second mistake is treating this as one decision. It isn’t. The right answer for the first sixty seconds of a session — before a user has decided to care — is often different from the right answer once they have summoned a developer and are two hours in. Design the boundary between those phases deliberately, and you can start with the fastest option and hand off to the most faithful one without the user noticing the seam.&lt;/p&gt;
&lt;p&gt;If you are building something in this space and want a second opinion on where that seam should go, &lt;a href=&quot;/contact&quot;&gt;tell me what you’re building&lt;/a&gt;. It is the conversation I most enjoy having.&lt;/p&gt;</content:encoded><category>ai</category><category>developer-tools</category><category>architecture</category></item></channel></rss>