Skip to main content

Command Palette

Search for a command to run...

Your Coding Agent Needs a Reversible Backend, Not a Bigger Prompt

Updated
10 min readView as Markdown
Your Coding Agent Needs a Reversible Backend, Not a Bigger Prompt
E
Indie dev & technical writer. Building tools at the intersection of AI and developer experience. 10 years of full-stack engineering. Sharing what I learn along the way.

A repository branch gives an agent a private copy of source code. A pull request shows the diff. Continuous integration runs checks before anyone merges it. A direct connection to a shared backend has none of those boundaries. The agent can alter a schema, change an authentication rule, replace a storage policy, or deploy a function while the repository remains clean.

That mismatch is the problem behind a recent Hacker News discussion of InsForge, an open source backend platform aimed at coding agents. Its proposed backend branching model treats infrastructure and application state more like source code: an agent works on an isolated copy, a human reviews the resulting changes, and the branch can be merged or discarded. The useful idea is larger than one product. If an agent can write to a live backend, the backend needs a reversible change model.

A longer instruction saying "be careful with production" does not create that model. It only asks a probabilistic system to remember a warning at the same time that it is making many tool calls.

A repository branch is not a backend branch

A source branch protects files. A backend branch has to protect state that is spread across several services.

The database part includes tables, indexes, constraints, triggers, functions, views, and the rows that the application depends on. Authentication includes users, roles, providers, session settings, and access policies. Storage includes buckets, object metadata, and download rules. There may also be edge functions, scheduled jobs, feature flags, environment configuration, service endpoints, and deployment settings.

A new feature can touch several of these at once. Adding profile pictures might require a table, an object bucket, an authentication rule, an upload function, and a migration. A source diff can show the code for each operation, but it cannot by itself show the final state of the remote systems.

The difference between a source branch and a backend branch is therefore not just implementation detail. The backend copy must include the state that makes the code meaningful. If it contains only a temporary database while leaving authentication and storage shared, the agent still has a path to damage the parent environment.

The InsForge Show HN post presents backend branching as a response to this problem. Its description includes database, authentication, storage, functions, and schedules in the branchable unit. That is the right unit of thought even for teams that build the mechanism themselves.

Why passing tests can still miss damage

An agent can make a change that is syntactically correct, passes the visible tests, and is still unsafe.

Suppose a migration adds a non-null constraint. The test database contains only clean fixture rows, so the migration passes. Production contains older records with missing values. The deployment then fails at the boundary, or someone writes a rushed data cleanup that removes information.

A column rename creates another gap. The application and its tests use the new name, but a nightly report or an infrequently used export still queries the old one. The failure appears hours after the agent has finished.

The same issue occurs with access control. A policy can be valid YAML and accepted by the service while accidentally allowing public reads of data that should remain private. Tests often check that an intended user can access a record. They do not always check every unintended user.

Index removal is a quieter example. Small fixtures do not represent production volume, so the test suite cannot reveal a query plan that becomes expensive after deployment.

These are state problems, not merely code problems. A test environment is an observation of one state. It cannot prove that a change is safe in another state. A branch containing a realistic copy, a preview diff, and the relevant telemetry gives the agent and the reviewer a much better observation point.

Treat backend changes as a state machine

The backend interface should make the change lifecycle explicit. A useful minimum is proposed, previewed, applied to a branch, approved, merged, rejected, discarded, and rolled back.

from dataclasses import dataclass, field
from enum import Enum
from typing import Any

class Status(str, Enum):
    PROPOSED = "proposed"
    PREVIEWED = "previewed"
    APPLIED = "applied"
    APPROVED = "approved"
    MERGED = "merged"
    DISCARDED = "discarded"
    ROLLED_BACK = "rolled_back"

@dataclass
class Change:
    change_id: str
    branch_id: str
    operations: list[dict[str, Any]] = field(default_factory=list)
    status: Status = Status.PROPOSED
    rollback_target: str | None = None

class Backend:
    def preview(self, change: Change) -> dict[str, Any]:
        change.status = Status.PREVIEWED
        return {"branch": change.branch_id, "operations": change.operations}

    def apply_to_branch(self, change: Change) -> None:
        if change.status is not Status.PREVIEWED:
            raise ValueError("preview is required before apply")
        change.status = Status.APPLIED

    def discard(self, change: Change) -> None:
        change.status = Status.DISCARDED

The important method is preview. It should execute the proposal against a copy-on-write snapshot or an equivalent isolated environment and return the resulting state diff. Merely echoing the agent's intended commands is not enough. The preview should show schema changes, data changes, policy changes, created resources, and validation failures.

Applying a change should affect only the branch. The agent can run the service, inspect logs, and make another proposal without touching the parent. Merge becomes a separate action with its own authorization. Discard must remove resources created by the branch instead of leaving abandoned buckets, functions, or schedules behind.

The state machine also gives the system a place to reject invalid transitions. A change that was never previewed should not be applied. A discarded branch should not be merged. A rollback should point to a recorded parent state rather than depend on a human reconstructing commands from memory.

Make permissions visible

The agent should not receive one broad credential and a paragraph of restrictions. The backend should evaluate each operation against a policy that distinguishes observation from mutation and reversible work from parent changes.

version: "1"
agent: "coding-session"
branch:
  create_on_first_write: true
  expires_after_hours: 24

allow:
  read:
    - database.schema
    - database.tables
    - auth.roles
    - storage.buckets
    - logs.recent
  branch_write:
    - database.tables
    - database.indexes
    - storage.objects
    - functions.source

gate:
  destructive:
    - database.drop_table
    - database.drop_column
    - storage.delete_bucket
    - auth.delete_user
  parent_write:
    - production.database
    - production.auth
    - production.storage

approval:
  destructive: human
  parent_write: two_people

The exact syntax will vary, but the separation matters. Reads can usually proceed without interruption. Writes can be automatic inside a disposable branch. Destructive actions should produce a reviewable diff. A parent or production change should require an approval that is bound to the exact change identifier, not a general permission that remains active for the rest of the session.

A branch limit and an expiration rule are practical safeguards. Agents retry. They lose context. They can also create a new environment every time a tool call fails. The backend should clean up stale branches and stop a single session from consuming unbounded resources.

Give the agent telemetry with the same interface

A branch without observation is just a different place to make mistakes. The agent needs access to the logs, traces, health signals, and audit records produced by its own change.

If a new function returns errors, the agent should be able to associate those errors with the branch and the change that created the function. If an index changes query behavior, the agent should be able to inspect the relevant query observations. If a deployment fails, the error should link back to the operation that changed the deployment configuration.

An audit event should record the actor, branch, change, target, before state, after state, decision, and parent relationship. This is useful to a reviewer, but it is also useful to the agent. A later tool call can answer "what changed before this failure?" from structured data instead of guessing from a long conversation.

A small event shape can keep the interface consistent:

from datetime import datetime, timezone


def audit_event(change_id: str, branch_id: str, action: str,
                target: str, before: object, after: object,
                decision: str) -> dict[str, object]:
    return {
        "change_id": change_id,
        "branch_id": branch_id,
        "action": action,
        "target": target,
        "before": before,
        "after": after,
        "decision": decision,
        "recorded_at": datetime.now(timezone.utc).isoformat(),
    }

The event should be append-only from the agent's point of view. If an agent can rewrite the record of its own actions, the audit trail cannot support recovery or review.

Approval should match risk

A useful approval system does not ask a person to confirm every schema read. That would train teams to click through warnings. It applies more friction as the blast radius increases.

A read of branch metadata can run automatically. A write to a development branch can also run automatically if the branch is isolated and expires. A destructive operation should pause for a reviewer who can see the exact before-and-after state. A merge into a shared or production parent may need two reviewers, a change window, or an explicit rollback check.

The review screen should render the result in terms a person can understand. For a database, show affected tables and constraints as well as the generated SQL. For authentication, show which users and policies gain or lose access. For storage, show bucket rules and object visibility. A raw command log is evidence, not a usable review.

Approval should bind to a snapshot. If the agent changes the branch after approval, the old approval must no longer authorize the new state. This is a small detail with a large effect: it prevents an approved low-risk diff from becoming a blanket ticket for later operations.

Keep deployment reversible

The deployment loop should make the safe path the normal path:

  1. The agent proposes a change and receives a branch.
  2. The backend records the operations and produces a preview.
  3. The agent runs tests and checks branch telemetry.
  4. A reviewer examines the diff and risk summary.
  5. The backend merges the approved snapshot into its parent.
  6. The system monitors the merge and keeps a rollback target.
  7. A rejected proposal discards its branch and its resources.

Rollback is not the same as reversing one database transaction. A backend merge may update a schema, a policy, a function, and a schedule. Recovery must restore those parts in a safe order and report any step that could not be reversed.

The merge and rollback records should be idempotent. If a network failure leaves the client unsure whether a merge completed, retrying the same change identifier should query the existing operation rather than apply it twice. The agent should receive a stable result such as merged, already merged, or rollback required.

Measure the system, not the model

Teams do not need invented productivity numbers to tell whether this design helps. They can measure properties of the control plane.

Reviewability asks how much information a person must read before approving a change. Large, cross-service diffs suggest that the agent's tasks are too broad or that the backend is hiding important state.

Blast radius asks which resources and users could be affected by a merge. The answer should be available before approval, not reconstructed after an incident.

Recovery time measures the complete rollback path, including approvals, cleanup, and verification. A rollback that exists only in documentation is not a recovery path.

Policy violations show where the agent repeatedly attempts actions it is not allowed to take. A high count can indicate poor instructions, unclear tool names, or a policy that does not match the actual job.

The goal is not to make the agent sound more confident. It is to make its mistakes containable. The source tree has branches, diffs, reviews, and resets because software work is allowed to be wrong before it becomes shared. An agent-operated backend needs the same treatment. A better prompt may improve behavior, but only a reversible backend gives the system a reliable way to inspect, approve, merge, and undo state changes.

Sources

A

The asymmetry is stated well: source code got branching, review and CI, and infrastructure state never did, so the safety we rely on is entirely a property of the artifact the agent happens to be editing. An agent changing an auth rule leaves a clean repo and a changed system, which is the worst possible combination for anyone trying to work out later what happened. Reversibility is the right primitive rather than a bigger prompt, and I would push on one thing: not everything is reversible in the same sense. Schema and policy changes can be branched and rolled back cleanly; data written under the wrong policy cannot be un-read, and an email sent by a deployed function cannot be recalled. So the boundary still needs a category of effects that require a human regardless of how good the undo story is. The other property worth demanding from a model like this is that a refusal is informative - if the agent is blocked, it should get enough structure back to narrow the request and retry, otherwise it routes around the boundary with a direct call and you are back where you started.