<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>AL+ · Thoughts</title>
  <subtitle>Things we have learned by building, inheriting, repairing, and maintaining software.</subtitle>
  <link href="https://alplustech.com/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://alplustech.com/thoughts/" rel="alternate" type="text/html"/>
  <id>https://alplustech.com/</id>
  <updated>2026-09-10T00:00:00.000Z</updated>
  <author><name>AL+</name><email>contact@alplustech.com</email></author>
  <rights>©  ALPLUS TECHNOLOGIES S.R.L.</rights>
  <entry>
    <title>A control nobody has watched fail is not a control</title>
    <link href="https://alplustech.com/thoughts/a-control-nobody-has-watched-fail-is-not-a-control/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/a-control-nobody-has-watched-fail-is-not-a-control/</id>
    <published>2026-09-10T00:00:00.000Z</published>
    <updated>2026-09-10T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>Lessons from a signup-abuse incident on a platform we took over: the previous developer&#39;s abuse controls existed in code, passed review, and did nothing. What made them inert, how we fixed them, and the practices we kept.</summary>
    <content type="html"><![CDATA[<p>A security control that has never been observed failing does not exist. It is a line of code that reads well. We relearned this recently on a platform we took over from its previous developer, when automated signups started creating accounts and sending emails to addresses nobody had verified. Every control meant to prevent that was present in the codebase we inherited. None of them worked, and the one that would have stopped most of it, the CAPTCHA, had been configured for a different subdomain from the one the signup form actually lived on.</p>
<p>This piece is about how the previous developer had built it, why each control was inert, how we fixed it, and the practices we kept afterwards. The details of the product are not the point; the pattern is, because it is the shape of most inherited systems we see.</p>
<h2>What happened, in general terms</h2>
<p>The platform has a public signup form. As built, submitting it created a durable account, a profile, and an email on the first request, before the address was verified. Bots found the form. Each hit left a real record in the database and sent real mail to an address the bot had chosen.</p>
<p>For the organization that owns the platform, this was not a data-hygiene problem. A trusted institution sending unsolicited email to harvested addresses is a reputation and data-protection problem. That framing decided the priority.</p>
<h2>How the previous developer did it, and why each control was inert</h2>
<p>Six separate things were true at once. Each looked fine on its own, and all of them had passed review before we arrived.</p>
<p><strong>The CAPTCHA was pointed at the wrong subdomain.</strong> A bot challenge was integrated into the form and its keys were present in the configuration. They had been issued for a different hostname from the one the form was served on, so the challenge never verified anything on the live site. This was the single largest failure: one configuration value, never checked against production, removed the outermost defence entirely.</p>
<p><strong>Account creation preceded verification.</strong> The form wrote permanent state first and asked for proof of ownership second. Anything that reaches the form can therefore create records.</p>
<p><strong>The rate limits were real and did nothing.</strong> The application used a cache-backed rate limiter. In the environment where the limits were tested, the cache store was a null store, so every limit silently allowed everything. Code review saw four rate-limit calls and moved on. No test ever exercised them.</p>
<p><strong>Behind a proxy, per-IP limits collapsed into one bucket.</strong> The application sat behind an edge network and a platform router with no trusted-proxy configuration. Every request resolved to the same upstream address, so a per-IP limit became a single global limit. IP is also the wrong identity for this product's users, many of whom sit behind one shared institutional address.</p>
<p><strong>Validation lived in the browser.</strong> Profile completeness relied on HTML <code>required</code> attributes and a controller predicate. A direct request, or an administrator editing a record, could persist a &quot;complete&quot; profile with no name and no organization.</p>
<p><strong>Lifecycle email had no eligibility gate.</strong> Welcome and follow-up emails keyed off the existence of an account rather than a verified, complete profile. Unverified accounts entered email journeys automatically.</p>
<p>None of these is exotic, and none of them was malicious. They are what happens when controls are designed, written, and reviewed, and never once watched doing their job in the environment where they matter.</p>
<h2>The question that finds them</h2>
<p>For every control, ask: <strong>what test would fail if this control were deleted?</strong> And for every control that depends on configuration: <strong>has anyone seen it reject something in production?</strong></p>
<p>If the answer to either is &quot;no&quot;, the control is unprotected. It may work today. It will not survive the next refactor, the next environment change, or the next person who does not know why it is there. Tests that assert wiring (the middleware is registered, the method is called) give false confidence. Tests that assert behaviour (the sixth request in a minute is rejected; a profile without a name cannot be saved through any path; the challenge fails on the real hostname without a valid token) are the only evidence a control exists.</p>
<p>We now treat &quot;unauthenticated endpoint that can cause an email&quot; as its own review category, with layered limits, a challenge verified server-side against the hostname it is served from, and a behavioural test for each layer.</p>
<h2>How we fixed it</h2>
<p>Every gate moved server-side, and every control became provable.</p>
<ul>
<li>The CAPTCHA keys were reissued for the hostname the form is actually served on, verification happens on the server, and a test asserts that a request without a valid token is rejected.</li>
<li>Signup holds pending state only until the one-time code is verified. Nothing durable, and no email beyond the code itself, before that.</li>
<li>Rate limits are tested with a real cache store, and are keyed on recipient and domain velocity and a global ceiling, not only on IP. The per-IP limit is documented as best-effort until the edge configuration is complete.</li>
<li>Trusted proxies are configured explicitly, and the boot-time check that enforces them is achievable, not merely strict. A &quot;fail closed&quot; default that nobody can satisfy pushes people toward pasting broad ranges; safe defaults have to be reachable.</li>
<li>Profile validation is enforced by the model, so the browser, the API, and the admin interface cannot disagree.</li>
<li>Lifecycle email checks eligibility (verified, complete, correct role) at send time, not at signup.</li>
</ul>
<p>Two things outside the code mattered as much.</p>
<p><strong>Independent review found what a green build did not.</strong> Two read-only review passes over our own remediation surfaced a permanent lockout in the one-time-code path, an unbounded audit read, a document embed broken by a tightened content-security policy, and a cleanup selector that could have deactivated a legitimate user. The build was green throughout. Our fixes needed the same scrutiny as the code we inherited.</p>
<p><strong>Real data disagreed with the fixtures.</strong> Running an import against the full third-party reference dataset, rather than the small fixture the previous developer had tested with, showed that public domain suffixes were being accepted as organizational evidence and that closed entities were being imported as active. Fixtures encode assumptions; the source encodes reality.</p>
<h2>Cleaning up without making it worse</h2>
<p>Removing the bot-created accounts was the most dangerous step, because the evidence for &quot;this account was never real&quot; initially lived in short-lived records that a retention job deletes daily. A legitimately verified user could have looked inactive.</p>
<p>The pattern we settled on is portable to any destructive maintenance task:</p>
<ol>
<li>Dry run by default, producing a reviewed digest.</li>
<li>An exact list of record identifiers, approved by a person.</li>
<li>Membership recomputed at execution time, so the approved list cannot drift.</li>
<li>Soft state change only. No deletion, so every rollback is a state change rather than a restore.</li>
<li>A durable, append-only audit entry with an allowlisted payload, so identifiers and codes cannot leak into the log.</li>
</ol>
<h2>Practices we kept</h2>
<ul>
<li><strong>Configuration is verified against production, not read.</strong> Keys, hostnames, and allowed origins are checked where they run. A value that is present is not a value that is correct.</li>
<li><strong>One seam per concern.</strong> A single session-creation path and a single audit interface, so a control cannot be half-applied across callers.</li>
<li><strong>Evidence is not entitlement.</strong> Reference data about organizations is labelled as evidence for a later review, never as a signup gate. Naming that boundary in code and documentation stops it from quietly becoming an access rule.</li>
<li><strong>Copy is part of correctness.</strong> A message that is right for sign-in (&quot;if we have an account for this address…&quot;) is wrong for signup. Reused strings drift when flows change.</li>
<li><strong>Provenance from day one.</strong> Imported records carry where they came from and a status. Reconstructing that later is expensive, and guessing it lets imports overwrite manual corrections.</li>
<li><strong>Say plainly what remains external.</strong> Edge configuration, challenge keys, and monitoring credentials belong to the client's infrastructure. Naming them as prerequisites, in an operations runbook kept in the repository, turned the handover into a document rather than a conversation.</li>
</ul>
<h2>The position</h2>
<p>Controls are claims until a test, a review, or production has watched them fail. When we take responsibility for a system someone else built, we look first for the controls that have never been exercised, and we assume they do not work until shown otherwise. It is why <a href="/thoughts/codebase-takeover-checklist/">the codebase takeover checklist</a> asks whether authorization is enforced on the server and whether anyone has seen each control reject a request, and why <a href="/software-maintenance/">security is part of how we maintain software</a> rather than a separate service.</p>
]]></content>
  </entry>
  <entry>
    <title>Codebase takeover checklist: what to inspect before accepting a system</title>
    <link href="https://alplustech.com/thoughts/codebase-takeover-checklist/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/codebase-takeover-checklist/</id>
    <published>2026-09-10T00:00:00.000Z</published>
    <updated>2026-09-10T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>A reusable checklist for taking over software from another developer, agency, or team: access, production, data, change safety, dependencies, security, knowledge, and cost. Use it before you agree to own the system.</summary>
    <content type="html"><![CDATA[<p>Before you accept responsibility for a system someone else built, inspect it. This is the checklist AL+ uses when <a href="/software-takeover/">taking over software</a>. It is written so that a founder, a CTO, or an incoming team can use it without us. Work through it in order; the first sections protect you from the failures that cannot be undone.</p>
<p>The checklist has eight parts. Each item is a question with a yes-or-no answer. A &quot;no&quot; is not a reason to refuse the system. It is a line in the plan.</p>
<h2>1. Access and ownership</h2>
<p>The most common takeover failure is discovering, weeks in, that a critical account belongs to someone who has left.</p>
<ul>
<li>Is the source code in a repository your company owns, with administrator rights held by your company?</li>
<li>Do you have owner-level access to hosting, DNS, the domain registrar, databases, object storage, and CDN?</li>
<li>Are email sending, payment providers, third-party APIs, app store accounts, and monitoring registered to company addresses, not personal ones?</li>
<li>Where do secrets live, and can you rotate every credential the previous developer had without an outage?</li>
<li>Which accounts are paid, on which card, and when does each renew or expire?</li>
<li>Are there licences (fonts, libraries, SaaS seats, map tiles) that will lapse when the previous team's accounts close?</li>
</ul>
<p>If any answer is &quot;we do not know&quot;, stop and find out before you change anything.</p>
<h2>2. Production reality</h2>
<p>The repository tells you what was intended. Production tells you what is true.</p>
<ul>
<li>What is deployed right now, from which commit, and does it match the main branch?</li>
<li>How is it deployed? By hand, by a script, by a pipeline? Can you repeat it?</li>
<li>What runs on a schedule: cron jobs, queues, scheduled functions, webhooks? Where are they configured?</li>
<li>Which external services does the system call, and which call it?</li>
<li>What do the logs and error trackers say has been failing quietly?</li>
<li>What environments exist besides production? Is there a staging environment that resembles it?</li>
</ul>
<h2>3. Data</h2>
<p>Data is the part of a system you cannot regenerate.</p>
<ul>
<li>Where does data live: databases, files, third-party services, spreadsheets?</li>
<li>How large is it, and how fast is it growing?</li>
<li>When was the last backup, and has a backup ever been restored somewhere and checked?</li>
<li>What personal data is held, under which legal basis, and where is it documented?</li>
<li>Are there migrations pending, half-applied, or applied by hand?</li>
<li>Can you export the data in a form you could move to another system?</li>
</ul>
<p>Restore a backup to a safe place during the review. A backup that has never been restored is a hope, not a backup.</p>
<h2>4. Change safety</h2>
<p>You will need to change the system. Find out how dangerous that is.</p>
<ul>
<li>Can you run the system on a developer machine from the repository alone?</li>
<li>Are there tests? Do they pass? What do they cover?</li>
<li>Is there a review step before code reaches production?</li>
<li>Can a bad release be rolled back, and has that been done before?</li>
<li>Does a trivial change (a typo in a footer) travel through the full path in under a day?</li>
</ul>
<p>If the answer to the last question is no, fixing that path is the first engineering task, before any feature.</p>
<h2>5. Dependencies and platform</h2>
<ul>
<li>What are the runtime, framework, and database versions, and when does each reach end of life?</li>
<li>How many dependencies are there, how old are they, and which are unmaintained?</li>
<li>Do any carry known vulnerabilities?</li>
<li>When was the last upgrade, and what broke?</li>
<li>Is anything pinned to a version for a reason nobody remembers?</li>
</ul>
<h2>6. Security basics</h2>
<p>This is not a penetration test. It is the minimum you must know before you own the system.</p>
<ul>
<li>Is authorization enforced on the server, or only in the interface?</li>
<li>Are secrets in the code or in the repository history?</li>
<li>Is user input validated before it reaches the database or the shell?</li>
<li>Who has access to production, and is that access individual and revocable?</li>
<li>Are there administrative endpoints, debug modes, or default credentials still enabled?</li>
<li>Is transport encrypted everywhere, including between services?</li>
</ul>
<h2>7. Knowledge</h2>
<ul>
<li>Is the previous developer reachable, and willing to answer questions for a fee?</li>
<li>Is there any written description of the architecture, the deployment, or the reasons behind major decisions?</li>
<li>Which parts of the system does nobody currently understand?</li>
<li>Which parts does everybody avoid touching, and why?</li>
<li>What incidents happened in the last year, and how were they resolved?</li>
</ul>
<p>Ask about decisions, not code: why this database, why this hosting, what they would change, what they were afraid of. Record the answers.</p>
<h2>8. Cost and fit</h2>
<ul>
<li>What does the system cost per month in hosting, licences, and third-party services?</li>
<li>What does it cost in engineering time to keep running today?</li>
<li>What does the business need from it in the next twelve months?</li>
<li>Is any large part of it serving a use case that no longer exists?</li>
<li>Is this a system to own, to <a href="/software-modernization/">modernize in place</a>, or to replace? The answer comes from the previous seven sections, not from the first meeting.</li>
</ul>
<h2>How to use the result</h2>
<p>Write the answers down as a single document, with a &quot;no&quot; list ranked by risk. The ranking becomes the first weeks of work: access and billing first, then backups and deployments, then the fragile parts, then the wanted ones. A takeover done in this order shows the business a new feature later than it hoped and an outage never.</p>
<p>If you would rather have us run this review and take the system over afterwards, <a href="/software-takeover/">that is what our software takeover service is</a>. If you want to run it yourself, the checklist is yours.</p>
]]></content>
  </entry>
  <entry>
    <title>Production-readiness checklist for AI-generated applications</title>
    <link href="https://alplustech.com/thoughts/production-readiness-checklist-ai-generated-applications/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/production-readiness-checklist-ai-generated-applications/</id>
    <published>2026-09-10T00:00:00.000Z</published>
    <updated>2026-09-10T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>Thirty concrete checks that separate a working AI-generated (vibe-coded) prototype from production software: security, data, failure handling, operations, coherence, dependencies, and ownership.</summary>
    <content type="html"><![CDATA[<p>An AI-generated application is production-ready when it can serve strangers, survive failure, protect its data, and be changed by someone who understands it. Most vibe-coded prototypes meet none of these conditions, and their builders cannot tell, because a demo with one honest user never exercises them.</p>
<p>This checklist is the set of criteria AL+ checks when <a href="/ai-generated-software/">productionizing an AI-generated application</a>. It applies equally to a prototype built by hand. Each item is verifiable; do not accept a &quot;probably&quot;.</p>
<h2>Security: does it protect data from anyone but its owner?</h2>
<ol>
<li>Every authorization decision is enforced on the server. Hiding a button in the interface is not authorization.</li>
<li>Database access rules (row-level security or the equivalent) are enabled and tested with a second account.</li>
<li>No secret, API key, or password is in the source code or in the repository history.</li>
<li>User input is validated and parameterized before it reaches the database, the file system, or a shell.</li>
<li>Administrative routes, debug modes, and sample accounts are removed or locked.</li>
<li>Rate limiting exists on login, signup, password reset, and any endpoint that costs money to call.</li>
<li>Access to production (hosting, database, secrets) is individual, minimal, and revocable.</li>
</ol>
<h2>Data: can it be lost, and can it be recovered?</h2>
<ol start="8">
<li>Backups run automatically, and one has been restored to a separate environment and checked.</li>
<li>Each kind of data has one home. The same fact is not stored in two tables or two services that can disagree.</li>
<li>Schema changes are migrations under version control, not edits made in a database console.</li>
<li>Personal data held is listed, with a reason for holding it and a way to delete it on request.</li>
<li>The free tier, quota, or plan the data lives on is known, along with what happens when it is exceeded.</li>
</ol>
<h2>Failure: what happens when something goes wrong?</h2>
<ol start="13">
<li>A failed external call produces a message the user can act on, not a blank screen.</li>
<li>Errors are recorded somewhere a human will see them, with enough context to reproduce them.</li>
<li>Timeouts and retries are deliberate; a slow third party cannot hang the whole application.</li>
<li>Background jobs that fail are visible, and a job that silently stops running raises an alert.</li>
<li>Someone is notified when the application is down, by a channel they actually read.</li>
</ol>
<h2>Operations: can it be run by someone other than its author?</h2>
<ol start="18">
<li>Deployment is repeatable from the repository, by script or pipeline, not by hand.</li>
<li>A deployment can be rolled back, and the rollback has been rehearsed once.</li>
<li>A staging environment exists that resembles production closely enough to catch mistakes.</li>
<li>Configuration is separate from code and documented, including every environment variable.</li>
<li>There is a written page covering how to deploy, roll back, restore, and whom to call at each provider.</li>
</ol>
<h2>Coherence: can it be changed without breaking something unrelated?</h2>
<ol start="23">
<li>There is one way to do each common thing (fetch data, handle auth, call an API), not three generated variations.</li>
<li>The parts that matter most (payments, permissions, data changes) have automated tests that run before deployment.</li>
<li>Someone can explain, in plain sentences, how a request travels through the system and where its data ends up.</li>
<li>Dead code, unused dependencies, and abandoned experiments have been removed.</li>
</ol>
<h2>Dependencies: will it still build next year?</h2>
<ol start="27">
<li>Runtime, framework, and major libraries are on supported versions with known end-of-life dates.</li>
<li>Dependencies with known vulnerabilities have been upgraded or replaced.</li>
<li>The project builds from a clean checkout on a machine that is not the author's.</li>
</ol>
<h2>Ownership: who answers for it?</h2>
<ol start="30">
<li>One named person or team is responsible for the application in production and has the access, knowledge, and time to act.</li>
</ol>
<h2>How to read the result</h2>
<p>The count matters less than the pattern. A prototype that fails items 1 to 7 is not ready for a single external user, whatever else is true. One that fails only items 23 to 29 is safe to run but expensive to change, which is a problem you can schedule.</p>
<p>We check all thirty before taking responsibility for an application, and we keep what already works: the goal is production readiness, not a rewrite. <a href="/ai-generated-software/">The service page describes how</a>; the longer argument is in <a href="/thoughts/ai-built-software-still-needs-an-engineer/">AI-built software still needs an engineer</a>.</p>
]]></content>
  </entry>
  <entry>
    <title>What a good software handover should contain</title>
    <link href="https://alplustech.com/thoughts/what-a-software-handover-should-contain/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/what-a-software-handover-should-contain/</id>
    <published>2026-09-10T00:00:00.000Z</published>
    <updated>2026-09-10T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>The seven artifacts an outgoing developer or agency should hand over with a software system: access, environment map, operating manual, architecture notes, decision log, open issues, and a recorded walkthrough.</summary>
    <content type="html"><![CDATA[<p>A software handover is complete when the incoming team can deploy, restore, and change the system without contacting the outgoing one. Most handovers deliver a repository and a goodbye. This is what a good one contains, in the order the incoming team will need it.</p>
<p>Use this list in two directions. If you are leaving a system, deliver these seven things. If you are receiving one, ask for them by name before the previous team's last day, because after that day every item costs ten times more to recover.</p>
<h2>1. Transferred ownership of every account</h2>
<p>Not shared logins: ownership. Repositories, hosting, DNS and registrar, databases, object storage, email delivery, payment providers, third-party APIs, app store accounts, monitoring, and any SaaS the system depends on, each transferred to an account controlled by the company that owns the software. A spreadsheet listing each service, its purpose, the owning account, the payment method, and the renewal date is the single most valuable document in the handover.</p>
<h2>2. An environment map</h2>
<p>One page that answers: what runs where. Production, staging, and any other environments; the servers or platforms behind each; the databases and stores they use; the scheduled jobs and where they are configured; the external services called and the services that call in. A diagram helps, but a list is enough. The test is whether an engineer who has never seen the system can point at every moving part.</p>
<h2>3. An operating manual</h2>
<p>How to deploy a change, and how to roll one back. How to restore a backup, with the last date it was rehearsed. How to rotate each secret. How to add an administrator. What to do when the site is down, the queue is stuck, or the database is full. Who to call at each provider. This document is short and boring, and its absence is why most inherited systems have their first outage in the first month.</p>
<h2>4. Architecture notes</h2>
<p>Not a specification. A few pages on how the system is put together and why: the main components, how a request travels through them, where the data lives, the parts that are fragile, and the parts nobody should touch without reading the notes first. Written by the people leaving, while they still remember.</p>
<h2>5. A decision log</h2>
<p>The decisions that shaped the system and the reasons behind them: why this database, why this hosting, why this framework, why that integration was built in-house. The reasons matter more than the decisions. A decision without its reason gets reversed by the next team for the same reason it was made.</p>
<h2>6. The open issues, honestly</h2>
<p>Known bugs, workarounds in place, deferred upgrades, dependencies near end of life, security concerns, and the things that keep the outgoing engineers up at night. An honest list here saves the incoming team months of discovery and protects the outgoing team's reputation when the issues surface anyway.</p>
<h2>7. A recorded walkthrough and a question window</h2>
<p>One or two recorded sessions walking through the environment map, the deployment, and the architecture notes, with questions. Then an agreed window, paid if necessary, during which the outgoing team answers questions by email. A few hours of the previous developer's time are worth weeks of archaeology.</p>
<h2>What to do if you receive less than this</h2>
<p>Most incoming teams receive item one partially and nothing else. In that case, run <a href="/thoughts/codebase-takeover-checklist/">the codebase takeover checklist</a> yourself and write these seven artifacts as you go. The first three are urgent; the rest can follow in the first months.</p>
<p>If you would rather have the incoming team be us, <a href="/software-takeover/">software takeover</a> is the service, and producing this handover for the next team is part of how we finish it.</p>
]]></content>
  </entry>
  <entry>
    <title>Software needs an owner</title>
    <link href="https://alplustech.com/thoughts/software-needs-an-owner/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/software-needs-an-owner/</id>
    <published>2026-09-01T00:00:00.000Z</published>
    <updated>2026-09-01T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>Software becomes expensive when nobody clearly owns it. Ownership is a role, not a repository permission, and most struggling systems are missing it.</summary>
    <content type="html"><![CDATA[<p>Software becomes expensive when nobody clearly owns it. Not when it is old, not when it is written in the wrong language, and not when it was built quickly. Those things make software harder. A missing owner makes it expensive.</p>
<h2>What an owner is</h2>
<p>An owner is one party who can answer three questions about a system without looking anything up for very long:</p>
<ol>
<li>How does it work, end to end, in production?</li>
<li>What is likely to break next, and what happens to users when it does?</li>
<li>What would it cost to change it in a specific way?</li>
</ol>
<p>The owner does not have to be the person who wrote the code. They rarely are. They are the person who is responsible for the system's behavior over time and has the access, knowledge, and authority to act on that responsibility.</p>
<p>An owner is a role, not a permission. Having admin rights on a repository is not ownership. Being the person everyone emails when something breaks is closer, but only if that person can also decide what to fix and when.</p>
<h2>How ownership goes missing</h2>
<p>We see the same patterns in almost every system we are asked to take over.</p>
<p><strong>The developer left.</strong> A freelancer, an early employee, or a small agency built the product and moved on. The code is fine. The knowledge left with them, and nobody was asked to pick it up.</p>
<p><strong>The product was built quickly.</strong> A prototype turned into a business faster than anyone planned. Decisions that made sense for a demo are now load-bearing, and nobody remembers which ones.</p>
<p><strong>The product was built with AI.</strong> A founder or a small team produced a working application in weeks using AI tools. It works. Nobody on the team can explain why, which is a different problem from it being badly written.</p>
<p><strong>Ownership was split.</strong> One vendor runs the infrastructure, another maintains the application, and the customer's own team owns the data. Each party is responsible for their part. Nobody is responsible for the system.</p>
<p>In every case the symptom is the same. Small changes take weeks. Estimates are wide and nervous. Incidents are resolved by restarting things. Every conversation about the software starts with &quot;we would need to look into that.&quot;</p>
<h2>Why this costs more than it looks</h2>
<p>Unowned software does not fail loudly. It fails through friction.</p>
<p>Each change carries an unknown risk, so changes are batched, delayed, or avoided. Features the business needs are quietly dropped. Security updates are postponed because nobody knows what they might break. When a dependency finally forces the issue, the upgrade is large, urgent, and done by someone learning the system under pressure.</p>
<p>The cost shows up as slower product decisions, as staff who route around the software instead of through it, and eventually as a proposal to rewrite the whole thing. Rewrites proposed for this reason are usually a purchase of ownership at the highest available price.</p>
<h2>What taking ownership looks like</h2>
<p>Ownership starts with understanding, not with changes.</p>
<p>The first weeks are spent reading code, mapping infrastructure, tracing how data moves, and writing down what nobody wrote down. We measure what the system actually does in production, because documentation and reality drift apart. We make a list of what is fragile and what is fine, and we resist the urge to fix things before the list is complete.</p>
<p>Then ownership becomes routine. Dependencies are updated on a schedule. Backups are tested, not assumed. Deployments become boring. Monitoring answers questions before users ask them. Changes have a known cost because someone knows the system.</p>
<p>None of this is glamorous. It is the difference between software that can be changed and software that can only be replaced.</p>
<h2>What we are arguing for</h2>
<p>Every piece of software that matters to a business should have a named owner who understands it and answers for it. If that owner is inside your organization, protect their time. If nobody fits the description, that is the problem to solve first; <a href="/software-takeover/">taking over an existing system</a> and <a href="/software-maintenance/">maintaining it long term</a> is the work AL+ exists for. Everything else, including whether to modernize, rewrite, or leave the system alone, is a decision an owner makes.</p>
]]></content>
  </entry>
  <entry>
    <title>Repair or rewrite?</title>
    <link href="https://alplustech.com/thoughts/repair-or-rewrite/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/repair-or-rewrite/</id>
    <published>2026-08-18T00:00:00.000Z</published>
    <updated>2026-09-10T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>A rewrite is a conclusion you reach after understanding a system, not a starting point. Most systems proposed for rewrite need an owner, a plan, and a few months of repair.</summary>
    <content type="html"><![CDATA[<p>A rewrite is a conclusion, not a starting point. Most of the systems we are asked to rewrite do not need it. They need an owner, a repair plan, and a few months of patient work. A few of them do need a rewrite, and the only way to tell the difference is to understand the system first.</p>
<h2>Why rewrites are proposed</h2>
<p>Rewrites are rarely proposed because a system is technically beyond repair. They are proposed because nobody on the current team understands the system, and rebuilding it feels like the only way to get that understanding back.</p>
<p>That is an honest motivation. It is also the most expensive way to achieve the goal. A rewrite recreates the knowledge by recreating the software, and everything the old system learned from years of production, every edge case and every fix, has to be learned again.</p>
<p>The second common reason is frustration. Slow changes, recurring incidents, and an unfamiliar stack make a fresh start attractive. The frustration is real. The remedy is usually ownership, not replacement.</p>
<h2>What we look at before deciding</h2>
<p>We do not answer the repair-or-rewrite question in the first meeting. We answer it after a review that covers five things.</p>
<p><strong>What the system does.</strong> Not what the documentation says or what the founder remembers. What it does in production, for which users, and how often. Many rewrites are scoped against imagined usage.</p>
<p><strong>Where the risk is.</strong> Which parts break, which parts nobody touches, and which parts hold data that cannot be lost. Risk is usually concentrated in a small part of the codebase.</p>
<p><strong>Whether it can be changed safely.</strong> Is there a way to deploy a change and know it worked? Are there tests, staging environments, backups, and rollbacks? If not, those come first regardless of the final decision, because a rewrite without them is a bigger version of the same problem.</p>
<p><strong>What the stack costs to keep.</strong> Frameworks and runtimes reach end of life. Hosting bills grow. Some stacks are hard to hire for. These are real costs, but they are measurable, and they are often smaller than a rewrite.</p>
<p><strong>What the business needs next.</strong> A system that needs three small features a year has different requirements from one that needs to double its capability. The rewrite decision depends on the destination, not only on the current state.</p>
<h2>When repair is the answer</h2>
<p>Repair is the answer more often than teams expect. The signs:</p>
<ul>
<li>The core data model is sound, even if the code around it is messy.</li>
<li>Problems are concentrated. Two or three modules cause most incidents.</li>
<li>The system is in production with real users, and the behavior they depend on is mostly undocumented.</li>
<li>The stack is old but supported, or can be upgraded in steps.</li>
</ul>
<p>Repair means establishing ownership, adding the safety equipment (tests around the risky parts, monitoring, reliable deployments), and then improving the system in place, one bounded change at a time. Users see stability first and improvements second. Nothing is thrown away until its replacement is proven.</p>
<h2>When a rewrite is the answer</h2>
<p>Some systems do need to be replaced. The signs are specific:</p>
<ul>
<li>The platform is unsupported and cannot be upgraded. A framework with no security fixes is a deadline.</li>
<li>The data model is wrong for what the business now does, and every feature fights it.</li>
<li>The system was built for a scale or a use case that no longer exists, and most of it is dead weight.</li>
<li>The cost of understanding it exceeds the cost of specifying it, which happens with generated or heavily copied code that has no coherent design.</li>
</ul>
<p>Even then, we rewrite in pieces where we can. A new system that takes over one responsibility at a time from the old one keeps the business running and keeps the decision reversible.</p>
<h2>A decision matrix</h2>
<p>The five review areas above reduce to a small table. Score each row for the system in front of you; the column with the most marks is the recommendation, and a single &quot;replace&quot; in the platform row overrides the rest.</p>
<table>
<thead>
<tr>
<th>Question</th>
<th>Points to repair</th>
<th>Points to modernize in place</th>
<th>Points to rewrite or replace</th>
</tr>
</thead>
<tbody>
<tr>
<td>Is the platform supported?</td>
<td>Yes, current versions</td>
<td>Yes, but several upgrade steps behind</td>
<td>No, and it cannot be upgraded</td>
</tr>
<tr>
<td>Does the data model fit the business?</td>
<td>Yes</td>
<td>Mostly; a few structures fight new features</td>
<td>No; every feature works around it</td>
</tr>
<tr>
<td>Is there a safe way to change it?</td>
<td>Yes</td>
<td>Partly; tests and rollback exist for some parts</td>
<td>No, and adding them costs more than the parts are worth</td>
</tr>
<tr>
<td>Where is the risk?</td>
<td>Concentrated in two or three modules</td>
<td>Spread across a layer that can be replaced alone</td>
<td>Everywhere; no coherent design to preserve</td>
</tr>
<tr>
<td>How much of it is still used?</td>
<td>Most of it</td>
<td>Most of it, with dead areas to delete</td>
<td>A minority; the rest serves cases that no longer exist</td>
</tr>
<tr>
<td>What does the business need next?</td>
<td>Small features and stability</td>
<td>Significant new capability on the same foundation</td>
<td>A different product</td>
</tr>
</tbody>
</table>
<p>Repair is <a href="/software-maintenance/">maintenance</a> with a plan. Modernizing in place is <a href="/software-modernization/">our modernization service</a>. A rewrite is still done in pieces where we can, with the old system running until each piece has proven itself.</p>
<h2>The position</h2>
<p>Understand the system, make it safe to change, then decide. Teams that skip to the rewrite skip the part where they learn what they are replacing, and that knowledge is the most expensive thing to rebuild.</p>
]]></content>
  </entry>
  <entry>
    <title>Taking over software after the developer leaves</title>
    <link href="https://alplustech.com/thoughts/taking-over-software-after-the-developer-leaves/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/taking-over-software-after-the-developer-leaves/</id>
    <published>2026-08-04T00:00:00.000Z</published>
    <updated>2026-08-04T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>A practical sequence for inheriting a system whose builder is gone: secure access, understand production, write down what nobody wrote, then change things.</summary>
    <content type="html"><![CDATA[<p>When the developer who built your software leaves, the software does not change. Your relationship to it does. You now own a system nobody on your side understands, and the order in which you do the next things matters more than how fast you do them.</p>
<p>This is the sequence we follow when we take over a system in that state.</p>
<h2>First: secure access before anything else</h2>
<p>Before reading a line of code, collect and verify access to everything the system depends on: source control, hosting, domain registrar, DNS, databases, email sending, payment providers, third-party APIs, app store accounts, and monitoring. Confirm that each account is owned by the company, not by a personal address that left with the developer.</p>
<p>Rotate credentials the departed developer had. Do it carefully, one system at a time, with a way to roll back. A rushed credential rotation is the most common way a takeover causes its first outage.</p>
<p>Check billing. Systems fail months after a departure because a card on a forgotten account expired.</p>
<h2>Second: understand production, not the repository</h2>
<p>The repository tells you what the developer intended. Production tells you what is true. Start there.</p>
<ul>
<li>What is actually deployed, from which commit, and does the repository match it?</li>
<li>What runs on a schedule? Cron jobs, queues, and scheduled functions are where surprises live.</li>
<li>What talks to what? Map every external service, webhook, and integration.</li>
<li>What data exists, where, how large, and when it was last backed up? Then restore a backup somewhere safe to prove it works.</li>
<li>What do the logs and error trackers say has been failing quietly?</li>
</ul>
<p>This phase produces a map. It usually reveals two or three things that were one bad day away from failing.</p>
<h2>Third: write down what nobody wrote down</h2>
<p>Every inherited system has knowledge that lived only in the developer's head. Recover as much as you can while it is recoverable. If the developer is reachable and willing, a few paid hours of questions are worth more than weeks of archaeology. Ask about the decisions, not the code: why this database, why this hosting, what they would change, what they were afraid of.</p>
<p>Then write the operating manual the system never had. How to deploy. How to roll back. How to restore. Who to call at each external provider. Where the secrets live. It does not need to be long. It needs to exist and to be correct.</p>
<h2>Fourth: make one change safely</h2>
<p>Before any real work, make a trivial change and ship it through the full path: commit, review, test, deploy, verify, and know how you would undo it. If any step is missing or manual, fix that first. The ability to change the system safely is the foundation for everything else, and it is worth a week of effort even if the change itself is a typo in a footer.</p>
<h2>Fifth: address the fragile parts, then the wanted ones</h2>
<p>Now the map from step two becomes a plan. Expired certificates, unsupported dependencies, failing backups, and single points of failure come first. They are rarely what the business asked for and always what it needs.</p>
<p>Feature work starts after that. It goes faster than expected, because by now someone understands the system, and estimates are based on knowledge instead of hope.</p>
<h2>What to expect</h2>
<p>A takeover done in this order takes several weeks before the business sees a new feature. That is the cost of not doing it, made visible. Teams that skip to feature work inherit every risk the previous developer was silently managing and discover them one incident at a time.</p>
<p>The goal is not to replace the departed developer. It is to make the system not depend on any single person again. The full list of what to inspect is in <a href="/thoughts/codebase-takeover-checklist/">the codebase takeover checklist</a>, and <a href="/software-takeover/">software takeover</a> is the service when you would rather we did it.</p>
]]></content>
  </entry>
  <entry>
    <title>AI-built software still needs an engineer</title>
    <link href="https://alplustech.com/thoughts/ai-built-software-still-needs-an-engineer/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/ai-built-software-still-needs-an-engineer/</id>
    <published>2026-07-21T00:00:00.000Z</published>
    <updated>2026-07-21T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>AI changes how code is produced. It does not remove technical responsibility. Software generated quickly still needs someone who understands it and answers for it in production.</summary>
    <content type="html"><![CDATA[<p>AI changes how code is produced. It does not remove technical responsibility. A working application built in a week with AI tools has the same obligations as any other software in production: it has to stay up, protect its data, survive its dependencies, and be changeable by someone who understands it. Those obligations do not disappear because the code was cheap to write.</p>
<h2>What has actually changed</h2>
<p>Producing code used to be the expensive part. Now it is often the cheap part. A founder with a clear idea can have a functioning product without hiring a developer, and many do. That is a real change and mostly a good one. More things get built, and more of them get built by the people who understand the problem.</p>
<p>What has not changed is everything that happens after the code exists. Deployment, monitoring, backups, security updates, access control, performance under load, data migrations, and the slow accumulation of changes that a real business demands. This work was never about typing speed. It is about understanding a system well enough to be responsible for it.</p>
<h2>Where AI-built software gets into trouble</h2>
<p>We have taken over enough AI-built systems to see the same problems repeat. None of them are about code quality in the usual sense.</p>
<p><strong>Nobody can explain the system.</strong> The team can describe what they asked for. They cannot describe what they got. When something breaks, the debugging conversation starts from zero every time.</p>
<p><strong>The design is local, not global.</strong> Each feature was generated to satisfy its own prompt. Together they form a system with three ways of doing the same thing, duplicated logic, and data that lives in more places than it should. Each piece is reasonable. The whole is hard to change.</p>
<p><strong>Security is assumed.</strong> Authentication, authorization, input validation, secrets handling, and rate limiting are present in the form the tool produced by default, which may or may not be the form the business needs. Nobody checked, because nobody knew what to check.</p>
<p><strong>Operations were never designed.</strong> There is a deployment because the hosting platform made one. There are no backups, no alerts, no staging environment, and no plan for the day the database fills up.</p>
<p><strong>Dependencies are frozen.</strong> The generated project pinned whatever versions existed that week. A year later, several are unsupported, and upgrading means understanding code nobody has read.</p>
<h2>This is not an argument against AI tools</h2>
<p>We use them. They are good at producing a first version, at explaining unfamiliar code, and at removing the tedious parts of engineering. The failure mode is not using the tools. It is believing that the tools have taken on the responsibility along with the typing.</p>
<p>They have not. Responsibility for software in production belongs to a person or a company who understands the system, and AI tools do not currently hold that role. Someone has to.</p>
<h2>What an engineer adds to AI-built software</h2>
<p>Not rewriting it. Usually the right move is to keep what works and add what is missing.</p>
<ul>
<li><strong>Understanding.</strong> Read the whole system and write down how it actually works, so the next change starts from knowledge.</li>
<li><strong>Coherence.</strong> Consolidate the three ways of doing one thing into one. Give the data one home.</li>
<li><strong>Safety.</strong> Review authentication and authorization by hand. Move secrets out of the code. Add validation where user input meets the database.</li>
<li><strong>Operations.</strong> Backups that are tested, monitoring that alerts a human, deployments that can be rolled back.</li>
<li><strong>A dependency plan.</strong> Upgrade what is unsupported, and schedule the rest.</li>
</ul>
<p>After this, the AI tools become more useful, not less, because changes land in a system someone understands and can verify.</p>
<h2>The position</h2>
<p>Code has become cheap. Responsibility has not. Software that a business depends on needs an engineer who understands it and answers for it, however the code was produced. Getting that in place early costs far less than discovering its absence during an incident. The concrete criteria are in <a href="/thoughts/production-readiness-checklist-ai-generated-applications/">the production-readiness checklist for AI-generated applications</a>; <a href="/ai-generated-software/">productionizing AI-generated software</a> is how we help.</p>
]]></content>
  </entry>
  <entry>
    <title>Maintenance is product work</title>
    <link href="https://alplustech.com/thoughts/maintenance-is-product-work/" rel="alternate" type="text/html"/>
    <id>https://alplustech.com/thoughts/maintenance-is-product-work/</id>
    <published>2026-07-07T00:00:00.000Z</published>
    <updated>2026-07-07T00:00:00.000Z</updated>
    <author><name>Paul Bădărău</name></author>
    <summary>Most of a product&#39;s life happens after launch. Treating maintenance as a cost to minimize is how software quietly degrades and how the best improvements never get made.</summary>
    <content type="html"><![CDATA[<p>Most of a product's life happens after launch. A product that ships in six months and runs for six years spends more than ninety percent of its life in what most budgets call maintenance. Treating that time as a cost to minimize is how software quietly degrades, and how the improvements users would have valued most never get made.</p>
<h2>The accounting problem</h2>
<p>Building is a project. It has a start, an end, a budget, and someone who is proud of it. Maintenance is a line item. It has a monthly figure that someone tries to reduce every year.</p>
<p>This framing makes a strange claim: that the product was finished at launch and everything after is upkeep. No product that has users is finished. Users find the edges. The market moves. Dependencies change under it. The business learns what it actually needed, which is never quite what it specified.</p>
<p>The work that responds to all of that is product work. It happens to occur after launch.</p>
<h2>What maintenance actually contains</h2>
<p>When we maintain a system, the work falls into four kinds, and only one of them is what the word suggests.</p>
<p><strong>Keeping it running.</strong> Dependency updates, security patches, certificate renewals, backup verification, capacity, incident response. This is upkeep, and it is not optional. Skipping it is borrowing at a high interest rate.</p>
<p><strong>Keeping it correct.</strong> Fixing bugs, yes, but mostly noticing where the software's behavior has drifted from what users need. Reports that used to be right. Integrations whose other side changed. Edge cases that became common cases as the business grew.</p>
<p><strong>Keeping it changeable.</strong> Removing complexity that no longer earns its place. Simplifying the parts that every change has to pass through. Improving tests and deployments so that the next feature costs less than the last one. This work is invisible in the product and decisive for its future.</p>
<p><strong>Making it better.</strong> Small improvements informed by real usage: a faster page that people visit every day, a form that fails less often, a workflow with one step fewer. These are the highest-return changes in most products, and they are only visible to someone who is paying attention after launch.</p>
<p>A maintenance budget that covers only the first kind produces software that runs, drifts, hardens, and eventually gets rewritten.</p>
<h2>Why this matters for who does the work</h2>
<p>If maintenance is upkeep, it can go to whoever is cheapest. If maintenance is product work, it needs people who understand the product and its users, and who are trusted to make decisions.</p>
<p>We think the second view is correct, and it changes how we work. The people who maintain a system are the people who know it best. They should be the ones proposing what to build next, because they see where the friction is. They should have authority to remove things, not only to add them. And their time should be protected from being consumed entirely by the first kind of work, because the other three are where the value is.</p>
<h2>What we do about it</h2>
<p>When we take responsibility for a system, we plan maintenance as a product function. Upkeep is scheduled and boring. A share of every month goes to keeping the system changeable. And we keep a short list of small improvements drawn from real usage, worked through steadily.</p>
<p>The result is a system that gets better with age instead of worse, and a business that never has to fund a rewrite to buy back the years it spent minimizing the maintenance line.</p>
<h2>The position</h2>
<p>Launch is the beginning. Budget, staff, and pay attention to the years after it as the product work they are, and most of the reasons for rewriting software go away. This is the shape of <a href="/software-maintenance/">software maintenance as AL+ practices it</a>.</p>
]]></content>
  </entry>
</feed>
