Creating Sandboxed Alpine.js Extensions Stored in SQL for Agent-Native

Agent-Native persists reusable Alpine.js mini-apps in a SQL-backed extensions table, validates them against a whitelist of safe directives, and renders them inside sandboxed iframes with strict CSP headers.

The BuilderIO/agent-native repository implements a unique extension system that allows AI agents to generate and store interactive UI components directly in SQL. These sandboxed Alpine.js extensions provide a secure way to embed reactive widgets—such as calculators, dashboards, or data visualizations—within chat transcripts without modifying the core application code.

Extension Architecture and SQL Schema

The extension system centers on a dedicated SQL table defined in packages/core/src/extensions/schema.ts. This schema stores the complete HTML content of each mini-app alongside metadata fields including id, title, content, and createdAt.

All extension operations flow through the action layer in packages/core/src/extensions/actions.ts. The create-extension action validates incoming HTML against a whitelist of safe Alpine directives, inserts a row into the extensions table, and returns the new extension ID. This SQL-backed approach ensures atomicity with other application state and enables real-time synchronization via the useDbSync polling layer.

The HTML Shell and Sandboxing

Security is enforced by packages/core/src/extensions/html-shell.ts, which wraps stored content in an iframe with a strict Content Security Policy. The shell loads Alpine.js from a trusted CDN and applies a sandbox attribute that disables top-level navigation and access to the parent DOM.

The whitelist of permitted directives—shared with the plan content system—lives in templates/plan/shared/plan-content.ts. Only declarative Alpine attributes like x-data, x-model, x-text, and @click are allowed. Any attempt to embed script tags, external resources, or unsafe event handlers triggers a validation error before the SQL insert occurs.

Creating and Storing Extensions

To persist a new Alpine.js component, the agent calls appAction with the create-extension command. The system validates the HTML content, checks URL safety via packages/core/src/extensions/url-safety.ts, and commits the record to the database.

import { appAction } from "@agent-native/core";

await appAction("create-extension", {
  title: "BMI Calculator",
  content: `
    <div x-data="{weight:0, height:0}">
      <label>Weight (kg) <input type="number" x-model.number="weight" /></label>
      <label>Height (cm) <input type="number" x-model.number="height" /></label>
      <p>BMI: <span x-text="(weight / ((height/100)**2)).toFixed(1)"></span></p>
    </div>
  `,
});

This action executes the SQL insert defined in packages/core/src/extensions/actions.ts, storing the HTML in the content column of the extensions table.

Rendering Extensions Inline

Agent-Native distinguishes between transient and persisted extensions. The render-inline-extension action displays a one-off Alpine component in the chat transcript without database storage, while show-extension-inline loads a persisted extension by ID.

// Render a temporary calculator without saving
await appAction("render-inline-extension", {
  content: `<div x-data="{count:0}"><button @click="count++">+</button> <span x-text="count"></span></div>`,
});

// Load a saved extension (id = "abc123")
await appAction("show-extension-inline", { extensionId: "abc123" });

Both methods utilize the HTML shell to maintain sandbox boundaries, ensuring that even malicious content cannot escape the iframe context.

Managing Extensions via SQL Queries

The list-extensions action retrieves all stored extensions for display in the UI, while update-extension modifies existing records. These operations use the generic dbQuery surface in packages/core/src/server/db.ts, making them compatible with both local SQLite and cloud PostgreSQL deployments.

// List all extensions excluding content payload
const list = await appAction("list-extensions", { includeContent: false });

// Update an existing extension
await appAction("update-extension", {
  extensionId: "abc123",
  title: "BMI Calculator – Updated",
  content: `<div x-data="{weight:0, height:0}">...</div>`,
});

The update action executes an UPDATE extensions SET ... WHERE id = $1 query, maintaining the same transaction consistency as the rest of the application state.

Summary

  • Extensions are stored in the SQL extensions table defined in packages/core/src/extensions/schema.ts with columns for id, title, content, and metadata.
  • The create-extension action in packages/core/src/extensions/actions.ts validates HTML against a whitelist of safe Alpine directives before persisting.
  • Sandboxing is enforced by packages/core/src/extensions/html-shell.ts using iframe isolation and strict CSP headers.
  • Developers can render transient UI with render-inline-extension or persist reusable components with create-extension and load them via show-extension-inline.
  • The SQL-backed architecture ensures atomic transactions, real-time sync, and compatibility across SQLite and PostgreSQL environments.

Frequently Asked Questions

How does Agent-Native prevent XSS attacks in stored extensions?

Agent-Native validates all extension HTML against a whitelist of safe Alpine directives located in templates/plan/shared/plan-content.ts. The system rejects any script tags, external resources, or unsafe event handlers. Additionally, the packages/core/src/extensions/html-shell.ts wraps content in a sandboxed iframe with a strict Content Security Policy that prevents script execution outside the trusted Alpine.js CDN.

Can extensions access the parent window or DOM?

No. Extensions run inside an iframe with sandbox attributes that disable access to the parent DOM and top-level navigation. The HTML shell explicitly sets security policies that keep the Alpine.js application isolated from the main Agent-Native interface, ensuring that extension code cannot read sensitive data or modify the parent application state.

What is the difference between render-inline-extension and create-extension?

The render-inline-extension action displays a temporary Alpine.js component in the chat transcript without writing to the database, ideal for one-off calculations or demonstrations. The create-extension action persists the HTML to the SQL extensions table, making the component reusable across sessions and accessible via the Extensions panel through list-extensions and show-extension-inline actions.

Why store extensions in SQL rather than files?

Storing extensions in SQL provides atomicity with other application state changes, enables real-time synchronization across clients via the useDbSync layer, and ensures portability between local SQLite and cloud PostgreSQL deployments. This approach keeps UI logic out of the source code repository while maintaining transactional consistency with documents and plans.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →