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

> Learn how agent native securely stores and runs sandboxed Alpine.js extensions in SQL. Explore mini-apps validated against a safe directive whitelist and rendered in isolated iframes with CSP.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-06-27

---

**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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/extensions/url-safety.ts), and commits the record to the database.

```typescript
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`](https://github.com/BuilderIO/agent-native/blob/main/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.

```typescript
// 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`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/server/db.ts), making them compatible with both local SQLite and cloud PostgreSQL deployments.

```typescript
// 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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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.