PostgreSQL as an Agent Event Store: Outbox Pattern, LISTEN/NOTIFY & CDC at Scale
How to turn PostgreSQL into a resilient event store for autonomous AI agent swarms using the Transactional Outbox pattern, LISTEN/NOTIFY, and CDC.
With the rise of code-generating AI agents like Claude Artifacts, v0, and Lovable, web applications increasingly allow users to prompt an LLM and immediately execute the resulting React components in a live browser preview.
However, rendering user-prompted, AI-generated code directly inside the host DOM tree is a critical security vulnerability. An adversarial user—or an LLM tricked via prompt injection—can emit malicious JavaScript designed to steal session cookies from `document.cookie`, read authorization tokens from `window.localStorage`, or initiate unauthorized background transfers against the host API.
This architectural guide details how to build a zero-trust **AI Micro-Frontend Sandbox** using double-isolated `<iframe>` sandboxes, strict Content Security Policy (CSP) headers, and typed `postMessage` RPC channels.
┌────────────────────────────────────────────────────────────────────────┐
│ ZERO-TRUST SANDBOX TOPOLOGY │
│ │
│ ┌───────────────────────────┐ ┌──────────────────────────┐ │
│ │ Host Application │ │ Sandboxed Iframe Preview │ │
│ │ Origin: app.domain.com │ │ Origin: null / isolated │ │
│ │ │ │ │ │
│ │ [Session Cookies] (🔒) │ │ <AI-Generated-Widget /> │ │
│ │ [Local Storage] (🔒) │ │ (No Cookie Access) │ │
│ │ │ │ (No Parent DOM Access) │ │
│ └─────────────┬─────────────┘ └─────────────▲────────────┘ │
│ │ │ │
│ │ Typed postMessage RPC Bus │ │
│ └─────────────────────────────────────┘ │
│ (Cryptographic Nonce Verification) │
└────────────────────────────────────────────────────────────────────────┘Before architecting the sandbox, we must categorize the primary attack vectors present in generative coding applications:
To eliminate all host access, the generative preview must run inside an `<iframe>` hosted on a **separate origin** (e.g., `sandbox-preview.domain.com`) with restrictive `sandbox` attribute parameters.
<iframe
id="ai-sandbox-preview"
src="https://sandbox.yourdomain.com/embed"
sandbox="allow-scripts allow-forms"
referrerpolicy="no-referrer"
loading="lazy"
title="AI Code Execution Sandbox"
class="w-full h-full border-0 rounded-xl"
/>Content-Security-Policy:
default-src 'none';
script-src 'nonce-d89f7a6b' 'unsafe-eval';
style-src 'unsafe-inline' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src data: https: blob:;
connect-src 'none';
frame-ancestors 'self' https://app.yourdomain.com;Because the sandboxed iframe cannot access the host window, all state synchronization, resizing events, and AST code payloads must travel across an asynchronous, type-safe RPC bridge.
// lib/sandbox/rpc-protocol.ts
import { z } from 'zod';
export const SandboxMessageSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('RENDER_PAYLOAD'),
payload: z.object({
componentCode: z.string(),
props: z.record(z.any()),
theme: z.enum(['light', 'dark']).default('dark')
}),
nonce: z.string()
}),
z.object({
type: z.literal('RESIZE_REQUEST'),
payload: z.object({
height: z.number().max(4000)
}),
nonce: z.string()
}),
z.object({
type: z.literal('RUNTIME_ERROR'),
payload: z.object({
message: z.string(),
stack: z.string().optional()
}),
nonce: z.string()
})
]);
export type SandboxMessage = z.infer<typeof SandboxMessageSchema>;
export class HostSandboxRPC {
private targetWindow: Window | null = null;
private targetOrigin: string;
private nonce: string;
constructor(targetOrigin: string, nonce: string) {
this.targetOrigin = targetOrigin;
this.nonce = nonce;
}
public bindIframe(iframeElement: HTMLIFrameElement) {
this.targetWindow = iframeElement.contentWindow;
}
public send(type: SandboxMessage['type'], payload: any): void {
if (!this.targetWindow) {
console.warn('Sandbox iframe window not ready.');
return;
}
const message: SandboxMessage = {
type,
payload,
nonce: this.nonce,
} as any;
this.targetWindow.postMessage(message, this.targetOrigin);
}
}If user-generated code contains an accidental or intentional infinite loop (e.g. `while(true)` in a `useEffect`), it freezes the sandbox iframe and spikes CPU usage.
We neutralize this using a **Watchdog Timer and Web Worker Compiler**:
// sandbox/runtime-watchdog.ts
export function createSafeExecutionEnvironment(rawCode: string, timeoutMs: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
worker.terminate();
reject(new Error(`Execution timed out after ${timeoutMs}ms (Infinite loop detected).`));
}, timeoutMs);
const blob = new Blob([rawCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
clearTimeout(timer);
resolve(e.data);
};
worker.onerror = (err) => {
clearTimeout(timer);
reject(err);
};
worker.postMessage({ action: 'EXECUTE' });
});
}When an LLM generates React components with arbitrary utility classes (e.g. `bg-emerald-500 hover:scale-105 transition-all shadow-2xl`), loading the full Tailwind CDN inside the sandbox introduces bundle bloat and global CSS leakage.
To ensure deterministic styling without leaking styles into the host application, the sandboxed document injects an isolated Tailwind JIT compiler running in-memory:
// sandbox/tailwind-compiler.ts
import { createTailwindCompiler } from '@tailwindcss/browser-runtime';
export class SandboxedStyleEngine {
private styleTag: HTMLStyleElement;
constructor() {
this.styleTag = document.createElement('style');
this.styleTag.id = 'sandboxed-generated-styles';
document.head.appendChild(this.styleTag);
}
public compileAndInject(htmlString: string): void {
try {
// Scanned utility classes are generated dynamically
const generatedCss = createTailwindCompiler().generate(htmlString);
this.styleTag.textContent = generatedCss;
} catch (err: any) {
console.warn('Tailwind JIT compile error:', err.message);
}
}
}To embed the sandboxed iframe into a responsive Next.js application, the host component must handle automatic height resizing (via `ResizeObserver` inside the iframe) and provide an instant crash recovery mechanism if the sandbox encounters an unhandled runtime error.
// components/sandbox/AIPreviewContainer.tsx
'use client';
import React, { useRef, useState, useEffect } from 'react';
import { HostSandboxRPC } from '@/lib/sandbox/rpc-protocol';
interface PreviewProps {
generatedCode: string;
sandboxUrl?: string;
}
export function AIPreviewContainer({
generatedCode,
sandboxUrl = 'https://sandbox.domain.com/embed',
}: PreviewProps) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [iframeHeight, setIframeHeight] = useState<number>(400);
const [hasError, setHasError] = useState<string | null>(null);
const [nonce] = useState(() => Math.random().toString(36).slice(2));
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const rpc = new HostSandboxRPC(sandboxUrl, nonce);
rpc.bindIframe(iframe);
// Listen for resize and error events from sandbox
const handleWindowMessage = (event: MessageEvent) => {
if (event.origin !== new URL(sandboxUrl).origin) return;
if (event.data?.nonce !== nonce) return;
if (event.data.type === 'RESIZE_REQUEST') {
setIframeHeight(Math.max(200, event.data.payload.height));
} else if (event.data.type === 'RUNTIME_ERROR') {
setHasError(event.data.payload.message);
}
};
window.addEventListener('message', handleWindowMessage);
// On iframe load, transmit the generated code AST
const onIframeLoaded = () => {
setHasError(null);
rpc.send('RENDER_PAYLOAD', {
componentCode: generatedCode,
props: {},
theme: 'dark',
});
};
iframe.addEventListener('load', onIframeLoaded);
return () => {
window.removeEventListener('message', handleWindowMessage);
iframe.removeEventListener('load', onIframeLoaded);
};
}, [generatedCode, sandboxUrl, nonce]);
return (
<div className="relative w-full rounded-2xl border border-slate-800 bg-slate-950 p-2 shadow-2xl overflow-hidden">
{hasError ? (
<div className="p-6 rounded-xl bg-red-950/40 border border-red-800/80 text-red-300">
<h4 className="font-bold text-red-200">Sandbox Execution Fault</h4>
<p className="text-sm font-mono mt-2">{hasError}</p>
</div>
) : (
<iframe
ref={iframeRef}
src={sandboxUrl}
sandbox="allow-scripts allow-forms"
style={{ height: `${iframeHeight}px` }}
className="w-full rounded-xl transition-all duration-200"
title="Sandboxed AI Execution View"
/>
)}
</div>
);
}When users iterate rapidly with an AI code assistant (e.g. generating 30 successive iterations of a dashboard widget), spawning dynamic `Blob` URLs and accumulating iframe DOM nodes can cause browser heap exhaustion (> 1.2GB RAM).
To prevent memory leaks:
// sandbox/lifecycle-cleanup.ts
export function tearDownSandboxFrame(iframe: HTMLIFrameElement): void {
try {
// Notify sandbox to cancel pending RAF loops and timers
iframe.contentWindow?.postMessage({ type: 'TEARDOWN' }, '*');
} catch {
// Suppress cross-origin boundary errors
}
// Clear memory references
iframe.src = 'about:blank';
iframe.remove();
}For scenarios where rendering visual DOM components is not required—such as executing user-defined AI transform functions, data parsers, or mathematical filter scripts—an even higher security isolation boundary is **WebAssembly-compiled QuickJS**.
QuickJS runs an embedded, sandboxed JavaScript micro-runtime inside a WebAssembly linear memory array. The sandboxed code possesses zero access to the browser's `window`, `document`, or network fetch APIs:
// sandbox/wasm-quickjs-sandbox.ts
import { getQuickJS } from 'quickjs-emscripten';
export async function executeWasmSandboxFunction(
untrustedCode: string,
inputData: any
): Promise<any> {
const QuickJS = await getQuickJS();
const vm = QuickJS.newContext();
try {
const fn = vm.unwrapResult(vm.evalCode(`(${untrustedCode})`));
const arg = vm.jsonToValue(inputData);
const result = vm.unwrapResult(vm.callFunction(fn, vm.undefined, arg));
const output = vm.dump(result);
fn.dispose();
arg.dispose();
result.dispose();
return output;
} finally {
vm.dispose();
}
}Because WebAssembly memory cannot access JavaScript object pointers outside its allocated memory block, code executed inside QuickJS WASM is mathematically immune to DOM clobbering, Prototype Pollution, and cookie theft.
Before deploying an AI code execution sandbox to production, ensure that your infrastructure satisfies the following compliance standards:
To guarantee that continuous deployment pipelines do not accidentally weaken the sandbox CSP or expose parent window globals, we run automated penetration tests via Playwright in GitHub Actions:
// e2e/sandbox-security.spec.ts
import { test, expect } from '@playwright/test';
test.describe('AI Micro-Frontend Sandbox Isolation Guarantee', () => {
test('Prevents malicious child code from reading parent document cookies', async ({ page }) => {
// 1. Set sensitive cookie in parent application
await page.goto('https://app.hamidayub.com');
await page.context().addCookies([
{ name: 'session_token', value: 'secret_jwt_payload_998', domain: 'app.hamidayub.com', path: '/' },
]);
// 2. Transmit prompt injection attacking parent storage
const maliciousPayload = `
<script>
try {
const stolenCookie = window.parent.document.cookie;
window.parent.postMessage({ type: 'EXFILTRATION', stolen: stolenCookie }, '*');
} catch (err) {
window.parent.postMessage({ type: 'ATTACK_BLOCKED', error: err.message }, '*');
}
</script>
`;
// 3. Verify that the browser cross-origin policy rejects parent DOM traversal
const messagePromise = page.waitForEvent('console');
await page.evaluate((code) => {
const rpc = (window as any).__SANDBOX_RPC__;
rpc.send('RENDER_PAYLOAD', { componentCode: code, props: {} });
}, maliciousPayload);
// 4. Assert that the attack failed cleanly with SecurityError
const frame = page.frameLocator('#ai-sandbox-preview');
await expect(frame.locator('body')).not.toContainText('secret_jwt_payload_998');
});
test('Terminates infinite while loop within 3000ms watchdog threshold', async ({ page }) => {
await page.goto('https://app.hamidayub.com');
const runawayLoopPayload = `
function InfiniteComponent() {
while (true) {}
return <div>Never renders</div>;
}
`;
await page.evaluate((code) => {
const rpc = (window as any).__SANDBOX_RPC__;
rpc.send('RENDER_PAYLOAD', { componentCode: code, props: {} });
}, runawayLoopPayload);
// Assert that the watchdog timeout error message renders
await expect(page.locator('.text-red-300')).toContainText(
'Execution timed out after 3000ms'
);
});
});To explore server-side generative UI rendering without iframes, see our [Master Guide on Generative UI with React Server Components](/guides/generative-ui-streaming-react-server-components-llms).
How to turn PostgreSQL into a resilient event store for autonomous AI agent swarms using the Transactional Outbox pattern, LISTEN/NOTIFY, and CDC.

How to build deterministic autonomous code refactoring agents using Tree-Sitter AST inversion, S-expression queries, and programmatic syntax mutations.
Architecture decisions, technical debt realities, and engineering perspectives that don't come from a marketing team.
Delivered when there's something worth saying. Not on a schedule.
No spam. Unsubscribe anytime. Your email stays private.