← All posts

How to Build an Agentic Blog Engine for Complex Regulatory Environments in Under 45 Minutes

Paddy O'Cybear14 min read
WHAT'S SHOWCASED
provided by infrastructure vs. what's left to you
Single Sign-OnInfrastructure provided
Durable ExecutionInfrastructure provided
BackupInfrastructure provided
High AvailabilityInfrastructure provided
AuditWhat’s leftMark which tables to track — statement-level triggers keep the append-only, six-year trail the database itself refuses to mutate.
SecurityWhat’s leftWrite the row-level security policies once; Postgres enforces them on every query — the database says no, not an if-statement.
ComplianceWhat’s leftMap controls to the evidence the infrastructure already emits — Chaperone walks the citations into the audit; you own the narrative.

TL;DR: A HIPAA-aligned publishing platform, built in forty-five minutes — but the speed isn't the point. Single sign-on, durable execution, backup, and high availability come free from infrastructure. Audit, security, and compliance don't live in the application either: each collapses to a few declarative primitives you can hand an auditor. The compliance surface moved off the app and into infrastructure you can read.

I built a HIPAA-aligned publishing platform in under forty-five minutes. Per-post sensitivity tags — Public, Internal, PHI. A two-stage approval that routes every post through clinical editorial and a compliance officer before it can publish. An immutable audit trail sized for HIPAA's six-year retention window. And a set of AI agents that scan drafts for protected health information, propose a classification, and moderate against policy before a human ever looks.

The forty-five minutes is the headline, so let me spend the rest of this post telling you why it's the least interesting thing about the build.

Because the number you should be suspicious of isn't the wall-clock. It's this one: almost none of the compliance logic is in the application. The access control isn't in my API. The immutability guarantee isn't in my code. The approval gating isn't a status column and a cron job. The identity handling isn't a login form I wrote. Every one of those — the load-bearing, auditable, career-ending-if-you-get-it-wrong parts — lives in infrastructure, as a declarative primitive I can point an auditor at.

That relocation is the whole trick. And it's only possible because Kilter changed the price of the right architecture.

The convenient architecture is a compliance debt you can't see yet

Here's how a regulated app normally gets built under a deadline. You need auth, so you add a users table and a password hash and a session cookie — you'll "harden it later." You need an audit trail, so you make an audit_log table and a sincere team promise never to UPDATE or DELETE it. You need an approval workflow, so you add a status column and a nightly cron that nudges things along. You need access control, so you sprinkle if (user.role === ...) through every handler.

Every one of those is the convenient choice. Each one also smears a compliance control across your application code, where it will quietly rot. The audit table that nothing enforces gets a "temporary" cleanup script. The role check that lives in seventeen handlers is correct in sixteen of them. The approval status gets set directly by a well-meaning admin endpoint that skips the workflow. None of this shows up in a demo. All of it shows up in an audit — or a breach.

The reason teams reach for the convenient architecture isn't ignorance. It's economics. The right architecture — database-enforced row security, an append-only store the database itself refuses to mutate, a real durable workflow engine, a real identity provider — has historically cost weeks of infrastructure work to stand up. Under a ship-date, "weeks of Ory and Temporal and Postgres RLS plumbing" loses to "a status column and an if-statement," every time. The convenient architecture wins because the right one is too expensive to reach.

Kilter's entire proposition is to invert that economics.

kilter up: the right architecture, provisioned

The build starts the way every Kilter build starts. One command composes a set of best-in-class, CNCF-grade services from the catalog and wires them together on a real cluster:

kilter
$ kilter init hipaablog # scaffold the primary app
$ kilter up # postgres, ory, temporal, garage, signoz — provisioned & wired
$ kilter run add-auth # Ory Kratos/Oathkeeper/Keto, bound to the app

That is the moment the economics flip. Standing up Postgres with the extensions I need, a full Ory stack (Kratos, Oathkeeper, Keto), a Temporal cluster, an S3-compatible object store, and an OTel/SigNoz observability pipeline is not a sprint anymore. It's the default. The kilter.yaml that declares it is boringly short:

yaml
name: hipaablog
services:
  - postgres        # RLS + immutability triggers
  - ory             # Kratos / Oathkeeper / Keto — identity & authz
  - temporal        # the approval lifecycle engine
  - garage          # S3-compatible object storage
  - redis
  - signoz          # OTel traces from day one
  - mailpit
 
# non-primary runtimes: own images, deployed in-cluster
components:
  - name: api       # Go — the *sole* database writer
    image: api
  - name: worker    # Python — the AI reviewer agents (Temporal activities)
    image: worker

When the right architecture costs one command instead of one sprint, you stop trading correctness for convenience. You get to pick the architecture that's right — and, it turns out, the right one is also dramatically smaller, because each infrastructure primitive absorbs a pile of application code you'd otherwise have written, tested, and been liable for.

Let me show you the four relocations.

1. Access control moves into the database

The rule is: a reader with internal clearance may never see a phi post. In the convenient architecture that rule lives in application code, re-implemented at every read path, correct until the day someone adds an eighteenth handler.

In the right architecture it's a property of the data itself. The sensitivity levels are a Postgres enum, declared low-to-high on purpose so that ordering encodes the access rule:

sql
-- declared low → high, so `sensitivity <= clearance` IS the access rule
CREATE TYPE sensitivity AS ENUM ('public', 'internal', 'phi');

Then Row-Level Security enforces it in the engine, for every query, forever. The Go API sets three GUCs per transaction — app.user_id, app.role, app.clearance — and the policy does the rest:

sql
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
 
CREATE POLICY posts_visibility ON posts
  USING (
        author_id   = current_setting('app.user_id')::uuid
     OR sensitivity <= current_setting('app.clearance', true)::sensitivity
     OR (status = 'published' AND sensitivity = 'public')
  );

There is exactly one way for this rule to be wrong now, and it's visible in a single file. There's a subtlety that makes it airtight: a single-writer rule. Only the Go API connects to Postgres, and it connects as a non-superuser role (hipaablog_app) that RLS actually applies to — the frontend gets no DATABASE_URL at all. The database, not the application, is the thing that says no. That's HIPAA §164.312(a) access control expressed as an infrastructure primitive instead of a code path.

2. Immutability and retention move into the engine

An audit trail you promise not to mutate is not an audit trail. The integrity requirement — §164.312(c) — says the record must be provably un-tampered. A team policy isn't proof. A RAISE EXCEPTION is.

So audit_log and post_versions are immutable because the database physically refuses to change them, via a statement-level trigger:

sql
CREATE FUNCTION audit_log_block_mutation() RETURNS trigger AS $$
BEGIN
  RAISE EXCEPTION 'audit_log is append-only: % not permitted', TG_OP;
END;
$$ LANGUAGE plpgsql;
 
CREATE TRIGGER audit_log_block
  BEFORE UPDATE OR DELETE OR TRUNCATE ON audit_log
  FOR EACH STATEMENT EXECUTE FUNCTION audit_log_block_mutation();

Every meaningful event lands here as a typed action, and the vocabulary itself tells you what the system considers compliance-relevant:

sql
CREATE TYPE audit_action AS ENUM (
  'login','logout','login_failed',
  'read_phi','read_internal','write_phi','write_internal',
  'create','update','delete',
  'submit','approve','return','reject','publish','withdraw','archive',
  'export','role_change','agent_run','mfa_challenge'
);

Note agent_run sitting in that list next to read_phi and approve. Every AI agent action is a first-class, audited event with the same provenance weight as a human's. When an agent touches PHI, the trail records it as exactly that.

3. The approval lifecycle moves into Temporal

author → editor/clinical review → compliance officer → publish is a stateful, long-running, human-in-the-loop process with timers, returns, and escalations. In the convenient architecture that's a status column plus a cron job plus a prayer that nothing sets the status out of band.

In the right architecture it's a Temporal workflow. The lifecycle is the workflow: durable, deterministic, replayable, and — crucially — the only thing that can advance a post's stage. There is no admin endpoint that skips it, because the state doesn't live in a column anyone can UPDATE; it lives in the workflow's execution history, which is itself an audit record. A compliance officer's approval is a signal, not a database write. The gate can't be sidestepped because the gate is the engine.

4. Identity moves into Ory — and agents become principals

The app hand-rolls no auth. Ory Kratos owns identity; in production Oathkeeper sits at the edge and injects a verified X-User-Id into every request via its header mutator, so the Go API never parses a cookie or trusts a client claim. Keto holds the relationship tuples for authorization. Rolling your own login form in a HIPAA context is a liability you simply decline to take on — §164.312(d) authentication, delegated to a primitive built by people who do only that.

And because identity is real, agents are first-class principals. The Python reviewer agents run as Temporal activities — PHI detection and redaction, sensitivity classification, summarization, policy moderation — each one invoked by the workflow, each run written to audit_log as agent_run with the human→agent provenance chain intact:

python
@activity.defn(name="phi.scan")
async def phi_scan(content: str) -> dict:
    """Detect PHI patterns. Returns findings + a redacted preview."""
    ...
 
@activity.defn(name="compliance.classify")
async def compliance_classify(content: str) -> dict:
    """Suggest a sensitivity classification + the required review path."""
    scan = await phi_scan(content)
    if scan["phi_detected"]:
        return {"sensitivity": "phi", "review_path": ["editor", "compliance"]}
    ...

The agent doesn't get a magic backdoor. It gets an Ory identity and lands in the same immutable trail as everyone else. That's what "agentic" should mean in a regulated system: the machine is a governed actor, not an exception to the governance.

What's actually left in the application

Add up the relocations and look at what remains. The frontend is a TanStack Start app — a disposable UI layer. The API is a thin Go service whose main job is to set three GUCs and write rows. The agents are five short Python functions. The compliance-critical logic — access control, immutability, retention, workflow gating, identity — is not in any of them. It's in Postgres policies and triggers, in Temporal histories, in Ory's rules. Canonical, battle-tested, independently auditable primitives.

That's the double win the title's forty-five minutes was hiding. Picking the right architecture (a) collapsed the application to almost nothing, because the hard parts were absorbed by infrastructure — and (b) moved the entire compliance surface off the app and into a layer you can read. The app got small because the compliance moved out.

And that second consequence is the one that changes everything downstream. Because now your controls aren't a story you tell an auditor. They're artifacts. An RLS policy is a few lines of SQL. An immutability trigger is a function you can print. A Temporal workflow definition is the approval process, literally. audit_log is a queryable, tamper-evident record. Your HIPAA technical safeguards have become declarative, inspectable infrastructure — which means, for the first time, something other than a human can vouch for them.

Chaperone: the tool that walks your app to the dance

Here's the thing about compliance evidence: when your controls are smeared through application code, no tool can attest to them. To prove access control is enforced, someone has to read every handler. To prove the audit log is immutable, someone has to trust a team policy. Compliance stays a manual, artisanal, point-in-time exercise — a binder assembled in a panic before the audit, stale the day after.

But move the controls into infrastructure and the picture inverts. The evidence is now machine-readable, because the controls are machine-readable. And that is exactly the seam Chaperone lives in — the tool that lets an application go to the dance in a regulated environment, and stay accompanied the whole night.

Chaperone reads the substrate, not the source. It walks the RLS policies, the immutability triggers, the Temporal workflow definitions, the audit_log schema, and the Ory rules, and maps each one to the control it satisfies:

Because the mapping is derived from live infrastructure, Chaperone does what a binder never could: it attests continuously. It watches the primitives and tells you the moment a control regresses — an RLS policy dropped in a migration, a trigger disabled, an audit action that stopped firing, a workflow stage that got a code path around it. Compliance stops being a snapshot you assemble under duress and becomes a property of the system that's true right now, provably, and checkable on every deploy.

And Chaperone can only do this because you built it right in the first place. There is no chaperone for an if-statement buried in a handler. There's no continuous attestation for a team promise not to run DELETE. Chaperone is not a scanner you bolt onto a mess to make it look compliant — it's the natural payoff of having put your controls where a machine can see them. The architecture earns the automation. You get the chaperone because you chose the right architecture, not the convenient one.

The forty-five minutes, reconsidered

So the speed was never the achievement. The speed is a symptom — evidence that the right architecture had gotten cheap enough to just… take. Kilter provisioned the Postgres, the Ory stack, the Temporal cluster, and the object store in one command, which meant the choice between "correct" and "convenient" stopped being a choice. They became the same thing.

And the correct architecture paid off twice: it shrank the application to a thin skin over canonical infrastructure, and it relocated every compliance control into that infrastructure — declarative, inspectable, and, therefore, automatable. Chaperone is what you build on top of that: continuous, derived-from-reality attestation that walks your app across the seam into regulated production and keeps vouching for it.

The convenient architecture asks you to trade correctness for a ship-date, then hands you the compliance bill years later, with interest. The right architecture, once it's cheap to reach, refuses the trade entirely — and leaves you with a system a machine can certify. That's the bet. Regulated software doesn't have to be slow, sprawling, and unprovable. It can be small, correct, and continuously attested — if you build it right, and if building it right is finally the easy thing to do.

Where to go next

  1. Start with Deploy a Sovereign App in Under 10 Minutes — the same kilter up composition, from an empty directory.
  2. Read The k8gentic Revolution — Kubernetes made simple, not easy: why exposing the real substrate is what makes the right architecture reachable.
  3. See Building an Inner-Enterprise Loop with Kilter — how kilter package carries a provisioned app across the seam into governed, audited production, the boundary Chaperone accompanies it through.
  4. Start your free trial — the full platform, 7 days, no card.