Getting StartedArchitecture OverviewExtension System Architecture

Extension System Architecture

Deep dive into how extensions are sandboxed, communicate with haex-vault, and integrate with the sync system.

Extension Overview

Extensions are sandboxed web applications that run within haex-vault. They can access vault data through the SDK while being isolated from the host system and each other.

Sandboxed Isolation

Extensions run in isolated environments with explicit permissions for each action.

Unified SDK

The @haex-space/vault-sdk provides a consistent API regardless of the display mode.

Own Database Tables

Extensions can create and manage their own tables with automatic CRDT sync.

Automatic Sync

Extension data is automatically synced across devices using the same CRDT system.

Display Modes

Extensions render either as a Tauri WebviewWindow (`window`) or an embedded HTML iframe (`iframe`). The `auto` mode picks per platform - window on desktop, iframe on mobile.

Aspect
window
iframe
RuntimeTauri WebviewWindowHTML iframe
PlatformDesktop (auto-picked)Mobile (auto-picked)
IsolationNative window + WebviewBrowser sandbox (parent origin checked)
CommunicationTauri invoke / eventspostMessage
DebuggingWebview DevToolsHost DevTools

Configure in Manifest

// manifest.json
{
  "displayMode": "auto"   // "auto" | "window" | "iframe"
  //
  // auto:    desktop → "window" (Tauri WebviewWindow),
  //          mobile  → "iframe"
  // window:  always open in a native window
  // iframe:  always render embedded inside haex-vault
}

Extension Lifecycle

Extensions go through a defined lifecycle from installation to runtime.

Cross-Device Installation:When you install an extension, the metadata is synced to other devices. However, the extension files must be downloaded separately on each device from the marketplace.

Permission System

Extensions must declare permissions in their manifest. Users grant or deny these permissions during installation.

// Internal representation of a granted permission
// (ts-rs binding: ExtensionPermission)
{
  id: string,
  extensionId: string,
  resourceType: 'db' | 'fs' | 'web' | 'shell'
              | 'filesync' | 'spaces' | 'identities' | 'passwords' | 'mail',
  action: { Database: 'read' | 'readWrite' | 'create' | 'delete' | 'alterDrop' }
        | { Filesystem: 'read' | 'readWrite' }
        | { Web: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '*' /* parsed but not enforced */ }
        | { Shell: 'execute' }
        | /* ... other resource types ... */,
  target: string,
  constraints: Record<string, unknown> | null,
  status: 'ask' | 'granted' | 'denied',
}

// Note: for Web permissions, only `target` (URL / domain pattern)
// is consulted in the runtime check today. The HTTP method action is
// parsed from the manifest but not enforced.

Permission Types

Database

  • read - SELECT only
  • readWrite - SELECT + INSERT/UPDATE
  • create - CREATE TABLE
  • delete - DELETE rows
  • alterDrop - ALTER / DROP TABLE

Filesystem

  • read - Read files
  • readWrite - Read + Write files

Web (HTTP)

  • (URL pattern) - HTTP method parsed but not enforced

Shell

  • execute - Run a configured shell command

Permission Enforcement

// Rust - Permission Check Flow (simplified)
//
// 1. Frontend sends an extension request through Tauri IPC.
// 2. The relevant command handler resolves the resource and action,
//    looks up a matching ExtensionPermission in haex_extension_permissions,
//    and routes by status.
fn handle_db_request(extension_id: &str, sql: &str) -> Result<...> {
    let target = parse_table_target(sql)?;            // e.g. "pkA__ext__items"
    let action = derive_db_action(sql)?;              // Read | ReadWrite | Create | Delete | AlterDrop

    let permission = permission_manager.lookup(
        extension_id,
        ResourceType::Db,
        action,
        &target,
    )?;

    match permission.status {
        PermissionStatus::Granted => run_sql(sql),
        PermissionStatus::Denied  => Err(PermissionDenied),
        PermissionStatus::Ask     => prompt_user_and_cache(permission),
    }
}

Extension Communication

Extensions communicate with the host through different transports depending on the display mode, but the SDK exposes the same API in both cases.

iframe Communication (postMessage)

// Iframe extensions talk to haex-vault via postMessage.
// The SDK wraps this. As an extension developer you use the client API
// (client.query, client.permissions.*, ...) - the protocol is internal.

// Conceptually, every SDK call becomes a request like this:
window.parent.postMessage({
  type: 'haextension:invoke',
  id: 'request-123',
  method: 'database.query',
  args: ['SELECT * FROM haex_passwords', []],
}, '*')

// And haex-vault answers with a response (event = MessageEvent from the iframe):
event.source.postMessage({
  type: 'haextension:response',
  id: 'request-123',
  success: true,
  data: [{ id: 'abc', title: 'Gmail' }],
}, '*')

Window Communication (Tauri WebviewWindow)

// Window/WebView extensions on desktop and mobile use the same
// SDK and the same `haextension:invoke` / `haextension:response`
// protocol. On native windows the messages are bridged through
// the Tauri WebviewWindow rather than a browser iframe.
//
// As an extension developer you never write this code by hand -
// the SDK handles it. The example below is illustrative only.
import { invoke } from '@tauri-apps/api/core'

// Issued by the SDK under the hood:
await invoke('extension_request', {
  method: 'database.query',
  args: ['SELECT * FROM haex_passwords', []],
})

Development Mode

During development, extensions can be loaded from a local dev server with hot reload support.

Hot Reload

Changes to your extension code are reflected immediately without reinstalling.

Local-Only Data

Dev extension tables are NOT synced to other devices. They use CREATE TABLE IF NOT EXISTS for idempotency.

No CRDT Sync:Dev extension tables are local-only and don't push to haex-sync-server. Once you sign + install the extension, full sync support kicks in.

Receiving Sync Updates

Extensions can listen for sync events to reload their data when changes arrive from other devices.

// Inside an extension: listen for relevant sync updates
import { HAEXTENSION_EVENTS } from '@haex-space/vault-sdk'

const { client } = useHaexVaultSdk()

client.on(HAEXTENSION_EVENTS.SYNC_TABLES_UPDATED, async (event) => {
  const tables = event.data?.tables ?? []
  const ourPrefix = client.getTableName('') // '<pubKey>__<extName>__'

  if (tables.some((t) => t.startsWith(ourPrefix))) {
    await reloadDataAsync()
  }
})

Automatic Table Prefix:Extension tables are automatically prefixed with the extension's public key and name, so you only receive events for your own tables.