# How to Create Sandboxed Extensions Using Alpine.js in Agent-Native

> Learn to build secure sandboxed extensions with Alpine.js in Agent-Native. Explore five helper functions for safe host system interaction.

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

---

**Agent-Native extensions are self-contained Alpine.js applications that run inside sandboxed iframes, exposing five secure helper functions—`appAction`, `appFetch`, `extensionFetch`, `extensionData`, and `dbQuery`—to interact with the host system without compromising security.**

The `BuilderIO/agent-native` repository provides a **sandboxed mini-app primitive** called an *extension*. These extensions are stored in the SQL `tools` table (exposed via the `extensions` Drizzle export) and rendered inside isolated iframes. Because the code executes in a separate browsing context, it cannot access the DOM of the parent application or the host filesystem, making it safe to run arbitrary user-provided code.

## What Are Agent-Native Extensions?

An extension is an HTML snippet containing Alpine.js directives that Agent-Native renders inside a sandboxed iframe. According to the source code in [`.agents/skills/extensions/SKILL.md`](https://github.com/BuilderIO/agent-native/blob/main/.agents/skills/extensions/SKILL.md), the only way an extension can communicate with the rest of the application is through a carefully controlled set of injected JavaScript helpers. This architecture ensures that even if the extension contains malicious code, it remains confined to its iframe boundary.

## The Sandboxed Helper API

When an extension loads, Agent-Native injects several global helpers into the iframe context. These functions are defined in [`references/api.md`](https://github.com/BuilderIO/agent-native/blob/main/references/api.md) and provide controlled access to server-side resources.

### appAction(name, params)

Calls any server-side action defined in the `actions/` directory. Use this to trigger background jobs or fetch computed data from the server.

```javascript
const emails = await appAction('fetch-recent-emails', { limit: 10 });

```

### appFetch(path, options)

Makes authenticated requests to allowed internal endpoints under `/_agent-native/*`. This is useful for retrieving JSON previews of resources without invoking a full action.

```javascript
const preview = await appFetch('/_agent-native/preview/123');

```

### extensionFetch(url, options)

Also aliased as `toolFetch`, this helper proxies external HTTP requests through the server. It supports secure secret injection via `${keys.NAME}` placeholders, ensuring API keys never appear in client-side code.

```javascript
const response = await extensionFetch('https://api.github.com/user/repos', {
  headers: { Authorization: 'Bearer ${keys.GITHUB_TOKEN}' }
});

```

### extensionData

Also aliased as `toolData`, this provides a per-extension key-value store with automatic scoping (`user`, `org`, or `all`). It handles table creation, upserts, and queries automatically.

```javascript
await extensionData.set('notes', 'note-1', { title: 'My Note' });
const allNotes = await extensionData.list('notes'); // user-scoped by default

```

### dbQuery(sql, args) / dbExec(sql, args)

Executes direct SQL read or write operations against tables the extension has permission to access. Use this for custom reports or ad-hoc data manipulation when the higher-level `extensionData` API is insufficient.

```javascript
const results = await dbQuery('SELECT * FROM custom_table WHERE user_id = ?', [userId]);

```

## Creating Your First Extension

You can create an extension using either the CLI or the HTTP API, as implemented in [`actions/create-extension.ts`](https://github.com/BuilderIO/agent-native/blob/main/actions/create-extension.ts).

### Using the CLI

Run the `create-extension` action with the HTML content passed directly:

```bash
pnpm action create-extension \
  --name "GitHub PR Dashboard" \
  --description "Shows open PRs for the repo" \
  --content '<div x-data="{ prs: [] }">...</div>'

```

### Using the HTTP API

Send a POST request to the extensions endpoint:

```http
POST /_agent-native/extensions
Content-Type: application/json

{
  "name": "GitHub PR Dashboard",
  "description": "Shows open PRs",
  "content": "<div x-data=\\\"{ prs: [] }\\\">...</div>"
}

```

For large HTML bodies, use the `contentFromAttachment` field to reference a file by ID rather than embedding thousands of tokens in the JSON payload.

## Alpine.js Patterns for Extensions

Extensions use standard Alpine.js directives (`x-data`, `x-init`, `x-show`, `x-for`), but the source code in [`.agents/skills/extensions/SKILL.md`](https://github.com/BuilderIO/agent-native/blob/main/.agents/skills/extensions/SKILL.md) distinguishes between two complexity levels.

### Trivial Extensions

For simple widgets, place state directly in `x-data`:

```html
<div x-data="{ count: 0 }" class="p-4">
  <button x-on:click="count++" class="rounded-md bg-primary px-3 py-1 text-sm">
    Increment
  </button>
  <span x-text="count"></span>
</div>

```

### Non-Trivial Components

For complex logic, move the component definition into a `<script>` block and register it with `Alpine.data()`. This prevents the fragility of large inline objects and enables better code organization:

```html
<div x-data="noteApp" class="p-4">
  <input x-model="title" placeholder="Title" class="border"/>
  <textarea x-model="body" placeholder="Body" class="border mt-2"></textarea>
  <button x-on:click="save" class="mt-2 bg-primary text-white px-3 py-1">
    Save
  </button>
</div>

<script>
document.addEventListener('alpine:init', () => {
  Alpine.data('noteApp', () => ({
    title: '',
    body: '',
    async save() {
      await extensionData.set('notes', this.title, {
        title: this.title,
        body: this.body
      });
    }
  }));
});
</script>

```

## Persisting Data with extensionData

Rather than writing raw SQL, extensions should use the `extensionData` API. This helper automatically creates necessary tables, handles scoping, and provides an upsert interface.

```javascript
// Store data scoped to the current user
await extensionData.set('preferences', 'theme', { darkMode: true });

// Retrieve all records for the current user
const preferences = await extensionData.list('preferences');

```

The scoping mechanism supports three levels: `user` (default), `org`, and `all`, allowing you to share data appropriately across different visibility boundaries without schema modifications.

## Calling External APIs Securely

The `extensionFetch` helper enforces security by requiring secret placeholders to be wrapped in **single quotes**. This prevents browser-side evaluation of the template literal.

```javascript
async function loadGitHubData() {
  const res = await extensionFetch(
    'https://api.github.com/repos/${keys.GITHUB_OWNER}/${keys.GITHUB_REPO}/pulls',
    {
      headers: { Authorization: 'Bearer ${keys.GITHUB_TOKEN}' }
    }
  );
  return await res.json();
}

```

The server replaces `${keys.NAME}` with actual values from the secure keystore before making the request, ensuring credentials never leave the server environment.

## Updating Extensions Programmatically

To modify an existing extension, use the `update-extension` action defined in [`actions/update-extension.ts`](https://github.com/BuilderIO/agent-native/blob/main/actions/update-extension.ts). You can apply changes using JSON Patch operations or structured edits targeting named HTML comment sections.

### JSON Patch Approach

```http
PATCH /_agent-native/extensions/12345
Content-Type: application/json

{
  "patches": [
    { "find": "bg-primary", "replace": "bg-accent" }
  ]
}

```

### Named Section Edits

For more surgical updates, target specific comment blocks in the HTML:

```javascript
// Edit the content between <!-- agent-native:section my-chart --> tags
{
  "edits": [
    {
      "section": "my-chart",
      "content": "<div x-data='...'>...</div>"
    }
  ]
}

```

## Security Best Practices

When developing extensions for Agent-Native, adhere to these guidelines from [`.agents/skills/extensions/SKILL.md`](https://github.com/BuilderIO/agent-native/blob/main/.agents/skills/extensions/SKILL.md):

- **Never embed real secrets** in HTML or script blocks. Always use the `${keys.*}` pattern with `extensionFetch`.
- **Use default canvas padding** unless you specifically need full-bleed layouts, in which case use `data-tool-layout` or `data-tool-padding` attributes.
- **Prefer Tailwind color tokens** like `bg-background` and `text-foreground` to ensure automatic theme inheritance.
- **Handle loading and error states** explicitly to prevent blank or broken widgets from degrading the user experience.

## Summary

- **Agent-Native extensions** are Alpine.js applications running in sandboxed iframes, stored in the `tools` table and exposed via the `extensions` Drizzle export.
- Five secure helpers—`appAction`, `appFetch`, `extensionFetch`, `extensionData`, and `dbQuery`—provide controlled access to server resources without exposing the host environment.
- Create extensions via `pnpm action create-extension` or the `POST /_agent-native/extensions` endpoint.
- Use `extensionData` for automatic table creation and scoped persistence, and `extensionFetch` with `${keys.*}` placeholders for secure external API calls.
- Update existing extensions using JSON Patch or named section edits via the `update-extension` action.

## Frequently Asked Questions

### How do I store data between extension sessions?

Use the `extensionData` helper (aliased as `toolData`). It automatically creates tables and handles scoping at the user, organization, or global level. Call `extensionData.set(storeName, key, value)` to save data and `extensionData.list(storeName)` to retrieve it, as documented in [`references/api.md`](https://github.com/BuilderIO/agent-native/blob/main/references/api.md).

### Can extensions access the filesystem or host DOM?

No. Extensions run in sandboxed iframes that isolate them from the parent page and host filesystem. They can only interact with the system through the five injected helpers: `appAction`, `appFetch`, `extensionFetch`, `extensionData`, and `dbQuery`. This security model is enforced by the browser's iframe sandbox and Agent-Native's CSP policies.

### How do I update an extension without replacing the entire HTML?

Use the `update-extension` action with either a `patches` array for find-and-replace operations or the `edits` array to target specific named sections within HTML comment blocks. The latter is safer for complex extensions where you want to update only the chart or data table portion without touching the surrounding layout.

### Where should I put complex Alpine.js logic for my extension?

For non-trivial logic, define your component in a `<script>` block using `Alpine.data()` rather than putting large objects directly in `x-data`. This pattern, demonstrated in [`.agents/skills/extensions/SKILL.md`](https://github.com/BuilderIO/agent-native/blob/main/.agents/skills/extensions/SKILL.md), improves maintainability and prevents parsing errors from complex inline expressions.