DEV Community

Casey Li
Casey Li

Posted on

Free Inference Stops at the Persistence Boundary

Free-tier language models can sketch glue code. They should not author the persistence layer. That boundary is what keeps a cheap draft from becoming two competing truths about the same table.

A generator that has never paid for a bad migration will still volunteer one. It adds a column, guesses a type, skips the backfill, and renames a cache key as if old clients were a rumor. The tokens look free. The on-call is not.

A spare hotel key is a useful object. It opens a door. It does not prove the holder knows which walls are load-bearing. Schema files, migrations, lockfiles, and cache identity are load-bearing walls. Free inference can walk the hallway. It should not redraw the floor plan.

This article proposes a small, testable refusal gate that runs before any free model endpoint or free server process is invoked. The gate is boring on purpose. Boring is the isolation.

Persistence fails on a delay

Application glue usually fails loudly. A wrong helper throws. A wrong unique index waits. Two rows that were supposed to be one customer become a ticket with no stack trace and a spreadsheet of guesses.

Cache identity fails in a cousin pattern. A model asked to fix a stale payload often invents a new key prefix instead of invalidating the old one. The brownfield client still reads the previous document. Two serializers now describe one entity. The page looks randomly wrong, which is worse than a clean 500.

Lockfiles belong in the same class. A model that helpfully rewrites a constraint file is not reviewing a supply chain. It is shuffling names it does not own. That work wants the package manager that created the lock, a human reading the diff, and a change someone can actually revert.

None of this needs a glossary of agent jargon. It needs a refusal that can be tested.

A free workspace is a side canal

Some teams park draft generation on a workspace that offers free model access and a free server option. MonkeyCode is one such option.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The useful role is narrow. Draft a parser. Sketch a retry wrapper. Propose a log line. Keep that work away from canonical schema. A free server changes where the generator process sleeps. It does not shrink the blast radius of ALTER TABLE.

Treat the free path as a side canal, not the river. Tasks arrive as JSON. A local gate inspects paths, intent, and a few cheap patterns. Allowed jobs may continue to whatever free endpoint the team already uses. Refused jobs print a reason and exit nonzero. No remote call. No prompt that says be careful.

A proposed refusal gate

The following module is a proposal, not a control plane. It is meant for a laptop pre-hook or a CI job. Matches are conservative. A false positive is cheaper than a silent migration.

#!/usr/bin/env python3
"""Refuse free-tier generation for persistence-shaped work.

Proposed example. Unexecuted against any vendor API in this article.
"""
from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path

PERSISTENCE_PATHS = (
    re.compile(r"(^|/)(migrations?|alembic|prisma|liquibase)(/|$)", re.I),
    re.compile(r"(^|/)(schema|models?|entities)(/|$)", re.I),
    re.compile(r"(docker-compose|compose)\.ya?ml$", re.I),
    re.compile(
        r"(package-lock|pnpm-lock|yarn\.lock|poetry\.lock|"
        r"Pipfile\.lock|go\.sum|Cargo\.lock)$"
    ),
)

PERSISTENCE_INTENT = re.compile(
    r"\b(migrat(e|ion)|alter\s+table|add\s+column|drop\s+index|"
    r"create\s+table|foreign\s+key|cache[-_ ]?key|ttl|invalidate|"
    r"unique\s+constraint|lockfile|pin\s+version)\b",
    re.I,
)

DANGEROUS_WRITE = re.compile(
    r"\b(apply|run|execute|deploy|push)\b.*\b(migrat|schema|prod)",
    re.I,
)


@dataclass(frozen=True)
class Verdict:
    allow: bool
    reason: str
    code: int  # 0 allow, 2 refuse, 3 malformed


def inspect_task(task: dict) -> Verdict:
    if not isinstance(task, dict):
        return Verdict(False, "task must be an object", 3)

    paths = task.get("paths") or []
    intent = str(task.get("intent") or "")
    target = str(task.get("target") or "")

    if target.lower() in {"prod", "production", "primary-db"}:
        return Verdict(False, "production target is a hard stop", 2)

    for raw in paths:
        text = str(raw)
        for rx in PERSISTENCE_PATHS:
            if rx.search(text):
                return Verdict(False, f"path looks like persistence: {text}", 2)

    blob = f"{intent}\n{target}\n" + "\n".join(str(p) for p in paths)
    if PERSISTENCE_INTENT.search(blob):
        return Verdict(False, "intent names schema, cache, or lockfile work", 2)
    if DANGEROUS_WRITE.search(blob):
        return Verdict(False, "intent asks to apply schema work", 2)

    return Verdict(True, "no persistence markers", 0)


def main(argv: list[str]) -> int:
    if len(argv) != 2:
        sys.stderr.write("usage: free_tier_gate.py TASK.json\n")
        return 3
    payload = json.loads(Path(argv[1]).read_text(encoding="utf-8"))
    verdict = inspect_task(payload)
    sys.stdout.write(json.dumps(verdict.__dict__, indent=2) + "\n")
    return verdict.code


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
Enter fullscreen mode Exit fullscreen mode

A refusing fixture can be this small.

{
  "id": "task-0412",
  "target": "staging",
  "intent": "Add a column for display name and invalidate the user cache key",
  "paths": [
    "app/models/user.py",
    "alembic/versions/20260907_display_name.py"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Run it without touching a network.

python3 free_tier_gate.py task-0412.json
echo $?
Enter fullscreen mode Exit fullscreen mode

Stdout should include "allow": false. The process should exit 2. An allowed neighbor might be a CSV preview helper under tools/ whose intent never names tables, TTLs, or locks. That job can travel to a free model. The fixture above should not.

A thin wrapper keeps the rule visible in shell history.

#!/usr/bin/env bash
set -euo pipefail
python3 free_tier_gate.py "$1"
status=$?
if [[ "$status" -eq 2 ]]; then
  echo "refused: keep this off free inference and off any free server" >&2
fi
exit "$status"
Enter fullscreen mode Exit fullscreen mode

Tests that keep the boundary from rotting

A gate without tests becomes folklore. The pytest module below is still a proposal. It pins three refusals and one allowance so a later edit cannot loosen the patterns in the name of convenience.

# test_free_tier_gate.py
from free_tier_gate import inspect_task


def test_refuses_alembic_path():
    v = inspect_task({
        "intent": "small cleanup",
        "paths": ["alembic/versions/20260907_init.py"],
        "target": "dev",
    })
    assert v.allow is False
    assert v.code == 2


def test_refuses_cache_intent_without_schema_path():
    v = inspect_task({
        "intent": "rotate the cache key for the profile payload",
        "paths": ["services/profile.py"],
        "target": "dev",
    })
    assert v.allow is False


def test_refuses_production_even_for_docs():
    v = inspect_task({
        "intent": "update README",
        "paths": ["README.md"],
        "target": "production",
    })
    assert v.allow is False


def test_allows_isolated_parser_sketch():
    v = inspect_task({
        "intent": "draft a CSV to dict helper with tests",
        "paths": ["tools/csv_preview.py", "tests/test_csv_preview.py"],
        "target": "dev",
    })
    assert v.allow is True
    assert v.code == 0
Enter fullscreen mode Exit fullscreen mode
pytest -q test_free_tier_gate.py
Enter fullscreen mode Exit fullscreen mode

If those four cases do not hold, the team does not have a boundary. It has a comment that used to be a script.

Red flags that mean the job already crossed

A request that names both a column and a cache prefix is not a coding task. It is a compatibility task. Free inference will optimize for the sentence that looks complete, not for the client that still hashes the old key.

A request that includes apply, migrate, and a hostname in one paragraph is operations work. An idle free server is not a migration runner with backups. The idle machine will still execute whatever DDL the model invented.

A request to make the model match production without a dumped schema is fiction. The generator will invent a plausible Postgres. Plausible is not the database sitting in front of the app.

A request to regenerate a lockfile because the build is red is supply-chain work. The package manager that wrote the lock is the alternative. A chat window is not an inventory of what the company may ship.

When those shapes appear, the exit is immediate. Stop the free-tier job. Copy the intent into a ticket. Hand it to someone who can see the live schema. Do not relax the regex. Do not add a system prompt about being careful with migrations. Prompts are not isolation.

Cheaper alternatives that stay honest

Draft the helper on the free path. Keep the migration in a checked-in template the team already trusts. Fill column names by hand. Generate backfill SQL from information_schema, not from a transcript.

For cache identity, keep one module of constants. Humans rename constants. Tests fail when a constant vanishes. A model that invents user:v3:profile beside user:profile will not run those tests unless the gate never lets it start.

# proposed cache identity module, not generated per request
PROFILE_KEY = "user:profile:v2"


def profile_cache_key(user_id: str) -> str:
    return f"{PROFILE_KEY}:{user_id}"
Enter fullscreen mode Exit fullscreen mode

For architecture the operator does not yet understand, stop generation entirely. Cheap code that encodes the wrong aggregate boundary is how technical debt arrives already compiled. Read the existing tables. Draw the foreign keys on paper. Then write the migration without a model in the loop.

Exit criteria for the whole habit

Abandon the free path for a given service when a refused job was smuggled around the gate. Abandon it when a cache incident traces to a generated prefix. Abandon it when a migration lands without a down step or a backfill. Abandon it when nobody can say which process is allowed to touch alembic/.

At that point the gate is theater. Move schema work to a reviewed, paid, or fully local toolchain. Leave free inference on parsers, fixtures, and docs.

Who should not use this

This refusal style is the wrong medicine for a throwaway SQLite file on a laptop with no users. It is also the wrong souvenir for a platform team that already has expand-contract migrations, backup-checked runners, and a DBA rotation. Those teams need their existing controls.

Do not treat an allow verdict as a skip-review card. A CSV parser can still read a path it should not see. The artifact only claims to keep persistence-shaped work off a free model and off a free server. It does not claim those environments are safe for secrets, production snapshots, or unsupervised agents.

Regexes will miss a migration hidden in a generic db/update.py. JSON can be laundered by renaming paths. This article does not measure latency, token volume, hardware, or uptime on any free server. Those figures move and are not claimed here. The claim is smaller. Persistence is a hard stop. A stop that cannot be tested is not a stop.

Operators who already route sketches through a MonkeyCode workspace can hang this script on the same local queue they use for free-model drafts, then keep schema work somewhere a human still has to type yes.

Top comments (0)