Lovable, v0, Bolt: they all nailed the same loop. You describe an app in chat, an agent writes the code, and a live preview updates while you watch. It feels like magic the first time. Then you try to use it for something real at work and hit the wall: your prompts, your source, and your data all live on someone else’s cloud.

For a lot of internal tools that’s a non-starter. The prototype a PM wants to spin up touches customer records. The dashboard someone needs by Friday runs against a production-adjacent database. You can’t paste that into a SaaS build tool and hope for the best.

So I built Kiln: the same describe-it-and-watch-it-build loop, self-hosted, with nothing leaving infrastructure you already run. No external SaaS dependency. It clones the Lovable build loop but supplies the database, the git backend, the deploy target, and the isolation itself, on top of the Kubernetes cluster, OIDC provider, and object storage you already have.

The Kiln workspace: describe a change on the left, watch the agent edit the code and the live app rebuild on the right.

The Kiln workspace: describe a change on the left, watch the agent edit the code and the live app rebuild on the right.

This is the third in a run of agent projects I’ve written up this year. 4shClaw and HiveLabs were about orchestrating teams of agents. Kiln is narrower and, honestly, harder: a single agent editing a real git repo, but wrapped in enough infrastructure that you’d trust it near your data.

The Loop

You start from the project picker: open an existing app, fork one, or start from a template or a curated example.

The project picker. Templates and examples seed the New Project flow so you’re not staring at a blank prompt.

The project picker. Templates and examples seed the New Project flow so you’re not staring at a blank prompt.

Name it, pick a starter template, and optionally toggle in a few additive features (auth pages, a calendar, file upload, a kanban board, a todo list, a signed webhook endpoint). The template you choose sets the project’s shape: a pure static client, a React + PocketBase app, a hybrid with a Fastify server alongside PocketBase, or a self-contained TypeScript backend with no PocketBase at all.

Creating a project: name it, choose a template, and toggle in additive features. The template picks the backend shape; the features scaffold real, owner-scoped functionality.

Creating a project: name it, choose a template, and toggle in additive features. The template picks the backend shape; the features scaffold real, owner-scoped functionality.

Pick the recommended template and you get a real project: a React + Vite + Tailwind frontend and an isolated PocketBase backend, committed to git. From there you just talk to it. “Build a classic 3x3 tic-tac-toe game: two players take turns, detect a win or a draw, add a New game button, and a scoreboard above the board tracking X wins, O wins, and draws across the session.” The agent reads the project conventions, thinks, edits the repo, commits, and the preview rebuilds and reloads. Every turn is a real commit on a real branch, not a diff floating in memory.

Watch it work: the agent reads the project’s conventions and structure first, then starts editing. The prompt that kicked it off stays pinned at the top of the transcript.

Watch it work: the agent reads the project’s conventions and structure first, then starts editing. The prompt that kicked it off stays pinned at the top of the transcript.

That “committed to git” is worth pausing on. Every Kiln project is a real repository, and Kiln hosts it itself: the platform runs its own internal Gitea, and each project is a repo on it. The builder clones that repo, the agent edits it, and the builder commits and pushes back. That repo is the source of truth for your app’s code, not a database blob, not a scratch directory that evaporates when the preview sleeps. The git host is part of the platform rather than something you bring, and because it’s a normal git host underneath, exporting or mirroring a project to an external remote is a straightforward feature.

That matters more than it sounds. Because the state of your app is a git history, you get restore anchors for free (every committing run drops a checkpoint you can roll back to), a Code tab that’s just the working tree, and an export path that isn’t a lie: it’s already a repo.

The Code tab shows the real diff from a turn: here the agent updated the app and its tests, committed, and kicked off the preview deploy. No magic hidden state.

The Code tab shows the real diff from a turn: here the agent updated the app and its tests, committed, and kicked off the preview deploy. No magic hidden state.

A minute later, the preview is a working game. Click a cell and X drops in, the turn indicator flips to O, the scoreboard tallies wins across games, and New game clears the board, exactly what the prompt asked for, running live in the iframe against the project’s own backend.

From one paragraph to a playable app: scoreboard up top, X in the top-left, O to move. This is the live preview, not a mockup.

From one paragraph to a playable app: scoreboard up top, X in the top-left, O to move. This is the live preview, not a mockup.

The One Decision Everything Hangs On

Here’s the part I’m most opinionated about, and the thing that separates “a fun demo” from “something you’d run near real data.”

Running an AI agent that writes and executes code is a security problem before it’s a product problem. The agent needs credentials: a model token, a git token, database access. The app it builds needs to actually run somewhere so you can see the preview. If you let those two things share a process, then any code the agent writes (or any prompt injection that steers it) runs with the agent’s credentials. That’s the whole ballgame.

So in Kiln they are never the same process. Each project’s data plane splits into two roles with a hard boundary between them:

RoleCredentialed?Job
builderyesclones the repo, runs the agent, commits, pushes to the internal Gitea
runnernoinstalls, builds, and serves the live preview

The builder holds the secrets. It gets the model token (filtered to an allow-list), a project-scoped git token, and a PocketBase admin token. It runs “dark”: telemetry is stripped before the agent subprocess even spawns. Platform-admin credentials are explicitly forbidden, and it will refuse to boot (throwing a CredentialScopeError) if a superadmin secret somehow leaks into its environment.

The runner is credential-free by construction. On boot it asserts that no model, git, or database secret exists anywhere in its environment and crashes immediately if one does. Its only source of truth is the committed code the builder handed it. It never talks to the model or the Gitea. As the internal note puts it, the runner “holds nothing worth stealing.”

This is the load-bearing wall of the whole system. The thing serving untrusted, AI-generated code on shared nodes has no secrets to leak. The thing with secrets isn’t reachable by the preview. Everything else, publishing, sharing, custom domains, is built on top of that split staying intact.

Desired State Lives in Postgres

The second decision I keep coming back to: Kubernetes objects are not the source of truth. Postgres is.

The control plane (a stateless Fastify orchestrator) holds desired state in Postgres and runs a reconcile loop that converges the real world toward it. Want a project’s preview running? There’s a row for that. Reconcile loop notices the gap and provisions the workloads. Idle draft? A policy scales it to zero and releases its edit lock, and the same loop tears the compute down while keeping the data volume.

Scale-to-zero in action. An idle draft’s preview gets reaped to save resources; your code and data are untouched, and resuming spins it back up.

Scale-to-zero in action. An idle draft’s preview gets reaped to save resources; your code and data are untouched, and resuming spins it back up.

The payoff is that provisioning goes through one backend-agnostic actuator interface with two implementations: one that drives Docker containers, one that drives Kubernetes Pods. Same reconcile logic, same hardening contract, same routing model, written once. docker compose up for the fast local loop; a kind cluster for the tier that actually matches production, with Calico-enforced network policies and ingress-by-subdomain.

That means “works on kind” isn’t a hand-wave. Because the Kubernetes path is the production path, exercising it locally exercises the real thing: the reconciler, the NetworkPolicies, the ingress, the scale-to-zero. I’ve been burned enough times by “it worked in dev” to want that property baked in.

The Backend You Don’t Have to Think About

Every project gets its own PocketBase: stock, pinned, and SHA-verified. It’s the database, auth, and file storage, one small binary per project with a retained data volume.

The Data tab proxies straight into it, so you can browse collections, edit records, and import or export CSV without leaving the workspace. When the agent adds a feature that needs persistence, it writes a PocketBase migration into the repo like it would any other file. Publish applies those migrations to the published environment. No separate “database step.”

The Data tab is the project’s PocketBase, proxied in. Browse and edit records, import CSV, all inside the workspace.

The Data tab is the project’s PocketBase, proxied in. Browse and edit records, import CSV, all inside the workspace.

Choosing internal PocketBase over “bring your own database as the backend” was deliberate, and it’s a trade-off I went back and forth on. Bring-your-own is more flexible but drags in credential management, connection isolation, and a much bigger blast radius. A pinned per-project binary keeps the isolation story simple and the escape hatch is an export/mirror feature, not a foot-gun in the default path.

Governance, Because Someone Has to Pay For It

Agents cost money, and a build tool that lets anyone burn tokens without a ceiling is a bad idea in a company. So the boring-but-necessary stuff is durable, not bolted on:

  • Usage is tracked per run and per model, feeding a per-project Usage tab and admin dashboards.
  • Quotas enforce spend, with a soft tier that warns and a hard tier that refuses.
  • Abuse flags and tenant suspensions gate misbehaving accounts, and public apps get flag-only content scans.
Per-project usage: estimated cost, tokens, cache hits, and a breakdown by model. Quotas sit on top of this.

Per-project usage: estimated cost, tokens, cache hits, and a breakdown by model. Quotas sit on top of this.

None of this is glamorous. It’s the difference between a demo and something an operator will let you run on their cluster.

Publish, Share, and the Annoying iframe Problem

When a draft is ready, Publish stands up a deterministic published environment: its own PocketBase and runner, no builder (published apps don’t get edited, so they don’t get credentials either). Apps default to private, gated behind your org SSO, and there’s an operator flag guarding whether public publishing is even allowed. Private apps can still mint signed, expiring shared links for someone outside the org.

One detail I’ll admit ate more time than I expected: the preview is a cross-origin iframe, and for a private draft you can’t just point the iframe at the gated host, because it would redirect to the org identity provider, which refuses to render in a frame. The fix is a short-lived, single-use, same-origin lander that sets the preview’s session cookie in-frame, which then means every in-place remount has to re-resolve a fresh one. It’s the kind of thing that’s invisible when it works and maddening when it doesn’t.

What’s Still Rough

I’m not going to pretend it’s finished. Generated v1 apps are client-side static output against the trusted PocketBase binary, which is exactly what keeps untrusted long-lived servers off shared nodes, but it also bounds what you can build today. Project members (editor/viewer sharing) are modelled in the schema but still a deferred surface. Custom domains only work on the Kubernetes tier. And the whole thing assumes you already run the supporting cast (a cluster, an OIDC provider, object storage), which is the point, but it also means Kiln is aimed at teams who have that, not at a solo hacker on a laptop.


The two agent projects before this were about breadth: many agents, coordinating. Kiln is about depth: one agent, doing something genuinely risky (writing and running code), wrapped in enough of a security and reconciliation story that you could point it at real work. The build loop is the easy, fun part. The two boundaries, credentialed builder versus credential-free runner, and desired-state-in-Postgres versus whatever the cluster happens to be doing, are what make it something more than a local toy.