Official SDK for building HaexHub extensions with cryptographic identity and granular permissions.
npm install @haexhub/sdk
# or
pnpm add @haexhub/sdk
# or
yarn add @haexhub/sdk# Create your project (any framework)
npm create vite@latest my-extension -- --template react-ts
# Install SDK
cd my-extension
npm install @haexhub/sdk
# Initialize extension structure
npx haexhub initThe haexhub init command creates:
haextension/directory withmanifest.json- Public/private keypair (
public.key,private.key) haextension.config.jsonfor development- Updates
.gitignoreto excludeprivate.key - Adds npm scripts (
ext:dev,ext:build)
Import the manifest in your app's entry point:
import manifest from './haextension/manifest.json'; // or '../haextension/manifest.json'
const { client } = useHaexHub({ manifest });# Development
npm run ext:dev
# Build & sign for production
npm run ext:buildImportant: Always use the setup hook system to initialize your extension (create tables, run migrations, etc.). This ensures all database tables are created before your app tries to query them.
Without setup hooks, your app might try to query tables before they exist, causing race conditions. The setup hook system guarantees:
- ✅ Tables are created before queries run
- ✅ Migrations complete before app loads
- ✅ No race conditions on first load
- ✅ Clean separation of setup logic
Nuxt/Vue - Setup in Pinia Store (Recommended)
You must register your setup hook BEFORE calling setupComplete(). The recommended pattern is to register the hook at store/component initialization time, then explicitly call setupComplete() to execute it.
Recommended approach for Nuxt: Register setup hook in a Pinia store:
// stores/haexhub.ts
import { defineStore } from 'pinia';
import * as schema from '~/database/schemas';
import manifest from '../../haextension/manifest.json';
// Import migration SQL files
const migrationFiles = import.meta.glob('../database/migrations/*.sql', {
query: '?raw',
import: 'default',
eager: true,
});
export const useHaexHubStore = defineStore('haexhub', () => {
const nuxtApp = useNuxtApp();
const haexhub = nuxtApp.$haexhub;
const orm = shallowRef(null);
// Step 1: Register setup hook FIRST
haexhub.client.onSetup(async () => {
// Convert migration files to SDK format
const migrations = Object.entries(migrationFiles).map(
([path, content]) => {
const fileName = path.split('/').pop()?.replace('.sql', '') || '';
return { name: fileName, sql: content };
}
);
console.log(`Running ${migrations.length} migration(s)`);
// Run migrations
await haexhub.client.runMigrationsAsync(
manifest.public_key,
manifest.name,
migrations
);
});
// Step 2: Initialize database and trigger setup
const initializeAsync = async () => {
orm.value = haexhub.client.initializeDatabase(schema);
// Step 3: Call setupComplete() to execute the hook
await haexhub.client.setupComplete();
console.log('Database ready');
};
return {
client: haexhub.client,
state: haexhub.state,
orm,
initializeAsync,
};
});Then in your app.vue:
<!-- app/app.vue -->
<template>
<div v-if="haexhubStore.state.isSetupComplete">
<NuxtPage />
</div>
<div v-else>
<p>Initializing extension...</p>
</div>
</template>
<script setup lang="ts">
const haexhubStore = useHaexHubStore();
onMounted(async () => {
await haexhubStore.initializeAsync();
});
</script>Key Points:
- Register the setup hook immediately when creating your store/client
- Call
setupComplete()explicitly when you're ready to run the setup isSetupCompletewill only becometrueafter the hook finishes executing- This ensures migrations complete before any database operations
Vue 3 (Non-Nuxt) - Setup in app.vue
<!-- app/app.vue -->
<template>
<div v-if="isSetupComplete">
<YourApp />
</div>
<div v-else>
<p>Initializing extension...</p>
</div>
</template>
<script setup lang="ts">
const isSetupComplete = ref(false);
onMounted(async () => {
const { client } = useHaexHub();
// Register setup function (runs once after SDK initialization)
// This is where you create tables, run migrations, etc.
client.onSetup(async () => {
console.log('[Setup] Creating database tables...');
// Example: Create tables using raw SQL
const tableName = client.getTableName('demo_table');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Or with Drizzle ORM:
// await createTablesAsync(client);
console.log('[Setup] Database tables created successfully');
});
// Wait for setup to complete before showing the app
console.log('[app.vue] Waiting for setup completion...');
await client.setupComplete();
console.log('[app.vue] Setup complete, app ready');
isSetupComplete.value = true;
});
</script>React - Setup in App component
// src/App.tsx
import { useState, useEffect } from 'react';
import { useHaexHub } from '@haexhub/sdk/react';
import manifest from './manifest.json';
function App() {
const { client, getTableName, isSetupComplete } = useHaexHub({ manifest });
// Register setup hook to initialize database
useEffect(() => {
if (!client) return;
// Register setup function (runs once after SDK initialization)
client.onSetup(async () => {
console.log('[Setup] Creating database tables...');
// Example: Create tables using raw SQL
const tableName = getTableName('demo_table');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('[Setup] Database tables created successfully');
});
}, [client, getTableName]);
// Show loading screen until setup completes
if (!isSetupComplete) {
return <div>Initializing extension...</div>;
}
return (
<div>
{/* Your app content */}
</div>
);
}Svelte - Setup in root component
<!-- src/App.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { initHaexHub, haexHub, isSetupComplete } from '@haexhub/sdk/svelte';
import manifest from '../haextension/manifest.json';
onMount(async () => {
// Initialize SDK with manifest
initHaexHub({ manifest });
// Register setup function (runs once after SDK initialization)
haexHub.client.onSetup(async () => {
console.log('[Setup] Creating database tables...');
const tableName = haexHub.getTableName('demo_table');
await haexHub.client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('[Setup] Database tables created successfully');
});
});
</script>
{#if $isSetupComplete}
<div>
<!-- Your app content -->
</div>
{:else}
<p>Initializing extension...</p>
{/if}Vite (Vanilla JS/TS) - Setup in main.ts
// src/main.ts
import { createHaexHubClient } from '@haexhub/sdk';
import manifest from '../haextension/manifest.json';
const client = createHaexHubClient({ manifest });
// Register setup function (runs once after SDK initialization)
client.onSetup(async () => {
console.log('[Setup] Creating database tables...');
const tableName = client.getTableName('demo_table');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('[Setup] Database tables created successfully');
});
// Wait for setup to complete before rendering app
await client.setupComplete();
console.log('[main.ts] Setup complete, rendering app');
// Now render your app
document.querySelector('#app')!.innerHTML = `
<h1>My Extension</h1>
`;For complex schemas, create a separate setup file:
// database/createTables.ts
export async function createTablesAsync(client: HaexHubClient) {
console.log('[Setup] Creating database tables...');
const tables = [
{ table: schema.users, name: 'users' },
{ table: schema.posts, name: 'posts' },
// ... more tables
];
for (const { table, name } of tables) {
const config = getTableConfig(table);
const tableName = config.name;
const createTableSQL = `CREATE TABLE IF NOT EXISTS "${tableName}" (...)`;
await client.execute(createTableSQL, []);
console.log(`[Setup] ✓ Table ${name} created/verified`);
}
}Then use it in your setup hook:
client.onSetup(async () => {
await createTablesAsync(client);
});Complete working examples for each framework:
- Nuxt: github.com/haexhub/haex-demo-nuxt
- React: github.com/haexhub/haex-demo-react
- Svelte: github.com/haexhub/haex-demo-svelte
- Vite: github.com/haexhub/haex-demo-vite
Each demo shows:
- ✅ Setup Hook System - Proper initialization with table creation
- ✅ Database operations (CREATE, INSERT, SELECT)
- ✅ Application context subscription (theme & locale)
- ✅ Manifest loading
- ✅ Framework-specific best practices
The HaexHub SDK provides framework-specific adapters for seamless integration with popular frameworks:
Vue 3 - Composable with reactive refs
npm install @haexhub/sdk<script setup lang="ts">
import { useHaexHub } from '@haexhub/sdk/vue';
import manifest from './manifest.json';
const { client, context, getTableName } = useHaexHub({ manifest });
// Watch for context changes (theme/locale from HaexHub)
watch(() => context.value, (ctx) => {
if (ctx) {
console.log('Theme:', ctx.theme); // 'light' or 'dark'
console.log('Locale:', ctx.locale); // 'en', 'de', etc.
// Update your app's theme
document.documentElement.classList.toggle('dark', ctx.theme === 'dark');
}
}, { immediate: true });
// Create your own table - no permissions needed!
// Tables are automatically namespaced with your extension's publicKey
const tableName = getTableName('users');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// Full read/write access to your own tables
await client.execute(
`INSERT INTO ${tableName} (id, name, email) VALUES (?, ?, ?)`,
[crypto.randomUUID(), 'John Doe', '[email protected]']
);
const users = await client.query<User>(`SELECT * FROM ${tableName}`);
</script>
<template>
<div>
<h1>My Extension</h1>
<p>Theme: {{ context?.theme }}</p>
<p>Locale: {{ context?.locale }}</p>
<p>Users: {{ users.length }}</p>
</div>
</template>React - Hook with automatic state updates
npm install @haexhub/sdkimport { useHaexHub } from '@haexhub/sdk/react';
import { useEffect, useState } from 'react';
import manifest from './manifest.json';
interface User {
id: string;
name: string;
email: string;
}
function App() {
const { client, context, getTableName } = useHaexHub({ manifest });
const [users, setUsers] = useState<User[]>([]);
// React to context changes (theme/locale from HaexHub)
useEffect(() => {
if (context) {
console.log('Theme:', context.theme); // 'light' or 'dark'
console.log('Locale:', context.locale); // 'en', 'de', etc.
// Update your app's theme
document.documentElement.classList.toggle('dark', context.theme === 'dark');
}
}, [context]);
useEffect(() => {
async function initializeDatabase() {
// Create your own table - no permissions needed!
// Tables are automatically namespaced with your extension's publicKey
const tableName = getTableName('users');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// Full read/write access to your own tables
await client.execute(
`INSERT INTO ${tableName} (id, name, email) VALUES (?, ?, ?)`,
[crypto.randomUUID(), 'John Doe', '[email protected]']
);
// Query users
const result = await client.query<User>(`SELECT * FROM ${tableName}`);
setUsers(result);
}
initializeDatabase();
}, [client, getTableName]);
return (
<div>
<h1>My Extension</h1>
<p>Theme: {context?.theme}</p>
<p>Locale: {context?.locale}</p>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}
export default App;Svelte - Stores with $-syntax reactivity
npm install @haexhub/sdk<script lang="ts">
import { onMount } from 'svelte';
import { initHaexHub, haexHub, context } from '@haexhub/sdk/svelte';
import manifest from '../haextension/manifest.json';
let users = [];
onMount(() => {
// Initialize SDK with manifest
initHaexHub({ manifest });
});
// React to context changes (theme/locale from HaexHub)
$: if ($context) {
console.log('Theme:', $context.theme); // 'light' or 'dark'
console.log('Locale:', $context.locale); // 'en', 'de', etc.
// Update your app's theme
document.documentElement.classList.toggle('dark', $context.theme === 'dark');
}
async function loadUsers() {
// Create your own table - no permissions needed!
// Tables are automatically namespaced with your extension's publicKey
const tableName = haexHub.getTableName('users');
await haexHub.client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// Full read/write access to your own tables
await haexHub.client.execute(
`INSERT INTO ${tableName} (id, name, email) VALUES (?, ?, ?)`,
[crypto.randomUUID(), 'John Doe', '[email protected]']
);
users = await haexHub.client.query(`SELECT * FROM ${tableName}`);
}
onMount(() => {
loadUsers();
});
</script>
<!-- Automatically reactive with $ syntax! -->
<h1>My Extension</h1>
<p>Theme: {$context?.theme}</p>
<p>Locale: {$context?.locale}</p>
<ul>
{#each users as user}
<li>{user.name} - {user.email}</li>
{/each}
</ul>Vanilla JS / Other Frameworks - Core SDK
npm install @haexhub/sdkimport { createHaexHubClient } from '@haexhub/sdk';
import manifest from '../haextension/manifest.json';
const client = createHaexHubClient({ manifest });
// Subscribe to context changes (theme/locale from HaexHub)
client.subscribe(() => {
const context = client.context;
if (context) {
console.log('Theme:', context.theme); // 'light' or 'dark'
console.log('Locale:', context.locale); // 'en', 'de', etc.
// Update your app's theme
document.documentElement.classList.toggle('dark', context.theme === 'dark');
}
});
// Create your own table - no permissions needed!
// Tables are automatically namespaced with your extension's publicKey
const tableName = client.getTableName('users');
await client.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
// Full read/write access to your own tables
await client.execute(
`INSERT INTO ${tableName} (id, name, email) VALUES (?, ?, ?)`,
[crypto.randomUUID(), 'John Doe', '[email protected]']
);
const users = await client.query(`SELECT * FROM ${tableName}`);
console.log(users);| Framework | Import Path | Features |
|---|---|---|
| Vue 3 | @haexhub/sdk/vue |
Composable with ref reactivity |
| React | @haexhub/sdk/react |
Hook with state management |
| Svelte | @haexhub/sdk/svelte |
Stores with $ syntax |
| Core | @haexhub/sdk |
Framework-agnostic client |
The SDK automatically includes polyfills for browser APIs that don't work in custom protocol contexts (haex-extension://). You don't need to do anything - just import the SDK and everything works!
✅ localStorage - In-memory fallback when blocked ✅ sessionStorage - No-op implementation ✅ Cookies - In-memory cookie store ✅ History API - Hash-based routing fallback for SPAs
When you import the SDK:
import { createHaexHubClient } from '@haexhub/sdk';
// Polyfills are automatically active!The polyfills detect whether the native APIs work and only activate if needed. This means:
- Zero configuration - Works out of the box
- Framework agnostic - Works with Vue, React, Svelte, etc.
- No performance impact - Only activates when necessary
- SPA-friendly - Includes history API patches for client-side routing
You can build your extension using any framework and any libraries without worrying about custom protocol restrictions. Things that "just work":
- Vuex, Pinia, Zustand (state management using localStorage)
- Vue Router, React Router (client-side routing)
- Cookie-based authentication libraries
- Any npm package that uses localStorage/cookies
HaexHub provides an Application Context that extensions can subscribe to for reactive updates:
interface ApplicationContext {
theme: 'light' | 'dark'; // User's theme preference
locale: string; // User's language (e.g., 'en', 'de', 'fr')
}Framework-specific subscription examples:
Vue 3 / Nuxt
<script setup lang="ts">
import { watch } from 'vue';
import { useHaexHub } from '@haexhub/sdk/vue';
import manifest from './manifest.json';
const { context } = useHaexHub({ manifest });
watch(() => context.value, (ctx) => {
if (ctx) {
// Update theme
document.documentElement.classList.toggle('dark', ctx.theme === 'dark');
// Update i18n locale
// i18n.locale.value = ctx.locale;
}
}, { immediate: true });
</script>React
import { useEffect } from 'react';
import { useHaexHub } from '@haexhub/sdk/react';
import manifest from './manifest.json';
function App() {
const { context } = useHaexHub({ manifest });
useEffect(() => {
if (context) {
// Update theme
document.documentElement.classList.toggle('dark', context.theme === 'dark');
// Update i18n language
// i18n.changeLanguage(context.locale);
}
}, [context]);
return <div>Theme: {context?.theme}</div>;
}Svelte
<script lang="ts">
import { initHaexHub, context } from '@haexhub/sdk/svelte';
import manifest from '../haextension/manifest.json';
initHaexHub({ manifest });
// Reactive statement - runs whenever $context changes
$: if ($context) {
// Update theme
document.documentElement.classList.toggle('dark', $context.theme === 'dark');
}
</script>
<p>Theme: {$context?.theme}</p>
<p>Locale: {$context?.locale}</p>Vanilla JS / Vite
import { createHaexHubClient } from '@haexhub/sdk';
import manifest from '../haextension/manifest.json';
const client = createHaexHubClient({ manifest });
// Subscribe to context changes
client.subscribe(() => {
const context = client.context;
if (context) {
// Update theme
document.documentElement.classList.toggle('dark', context.theme === 'dark');
// Update language
// updateLanguage(context.locale);
}
});Each extension is identified by a public key, not by name or namespace.
// Extension ID format: {publicKey}_{name}_{version}
// Example: MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV_password-manager_1.0.0Benefits:
- ✅ Mathematically unique (no collisions)
- ✅ Tamper-proof (signed with private key)
- ✅ Registry-independent (works everywhere)
All tables are automatically prefixed with your extension's identity:
const tableName = client.getTableName("users");
// Result: "MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV__my-extension__users"Naming Rules:
- Extension names and table names must start with a letter (a-z, A-Z)
- Can contain letters, numbers, hyphens (
-), and underscores (_) - Cannot contain double underscores (
__) - reserved as separator - Cannot contain dots (
.) - causes issues with SQL schema qualification - Must follow npm package naming conventions
Format: {publicKey}__{extensionName}__{tableName}
Own tables: Automatic full read/write access Dependency tables: Requires explicit permission
HaexHub uses a zero-trust permission model with automatic isolation:
Your extension can freely create, read, write, and delete its own tables without any permissions:
// ✅ Always works - no permissions needed!
const myTable = client.getTableName('users');
await client.database.createTable(myTable, '...'); // ✅ Create
await client.database.query(`SELECT * FROM ${myTable}`); // ✅ Read
await client.database.insert(myTable, {...}); // ✅ Write
await client.database.delete(myTable, 'id = ?', [1]); // ✅ DeleteWhy this is safe:
- Tables are automatically prefixed with your
publicKey(e.g.,MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV__myext__users) - Impossible to access other extensions' tables
- Complete sandbox isolation
- No manifest declarations required
To access another extension's tables, you must declare it in your manifest:
manifest.json:
{
"dependencies": [
{
"publicKey": "MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV",
"name": "password-manager",
"minVersion": "1.0.0",
"reason": "Access stored credentials",
"tables": [
{
"table": "credentials",
"operations": ["read"],
"reason": "Retrieve login data for email sync"
}
]
}
]
}Code:
// ❌ Would fail without permission!
const depTable = client.getDependencyTableName(
'a7f3b2c1d4e5f6a8b9c0',
'password-manager',
'credentials'
);
const creds = await client.database.query(`SELECT * FROM ${depTable}`);Permission Granularity:
- ✅ Per-table - Request access to specific tables only
- ✅ Per-operation -
["read"]or["read", "write"] - ✅ User consent - User approves each permission
- ✅ Revocable - User can revoke anytime
import { useHaexHub } from '@haexhub/sdk/vue';
const {
client, // Raw HaexHubClient instance
extensionInfo, // Readonly<Ref<ExtensionInfo | null>>
context, // Readonly<Ref<ApplicationContext | null>>
db, // DatabaseAPI
storage, // StorageAPI
getTableName // (tableName: string) => string
} = useHaexHub({ debug: true });
// Use in templates or computed
watch(() => extensionInfo.value, (info) => {
console.log('Extension:', info?.name);
});import { useHaexHub } from '@haexhub/sdk/react';
function MyComponent() {
const {
client, // HaexHubClient instance
extensionInfo, // ExtensionInfo | null
context, // ApplicationContext | null
db, // DatabaseAPI
storage, // StorageAPI
getTableName // (tableName: string) => string
} = useHaexHub({ debug: true });
// State automatically updates on SDK changes
return <div>{extensionInfo?.name}</div>;
}// Initialize once in +layout.svelte
import { initHaexHub } from '@haexhub/sdk/svelte';
initHaexHub({ debug: true });
// Use stores anywhere
import { extensionInfo, context, haexHub } from '@haexhub/sdk/svelte';
// In templates with $ syntax
<h1>{$extensionInfo?.name}</h1>
// In script
const tableName = haexHub.getTableName('users');
await haexHub.database.query(`SELECT * FROM ${tableName}`);import { createHaexHubClient } from '@haexhub/sdk';
const client = createHaexHubClient({
debug: true, // Optional: Enable debug logging
timeout: 30000 // Optional: Request timeout in ms
});// Subscribe to SDK updates
const unsubscribe = client.subscribe(() => {
console.log('Extension info:', client.extensionInfo);
console.log('Context:', client.context);
});
// Cleanup
unsubscribe();// Get your extension's info
const info = client.extensionInfo;
// {
// publicKey: "MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV",
// name: "my-extension",
// version: "1.0.0",
// namespace: "johndoe" // Display only
// }// Get table name for your extension
const myTable = client.getTableName("users");
// → "MCowBQYDK2VwAyEA7x8Z9Kq3mN2pL5tR8vW4yB6cE1fH3gJ9kM7nP0qS2uV__my-extension__users"
// Get table name for a dependency
const depTable = client.getDependencyTableName(
"MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k", // Dependency's publicKey
"password-manager", // Dependency's name
"credentials" // Table name
);
// → "MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k__password-manager__credentials"// SELECT queries
const users = await client.database.query<User>(
`SELECT * FROM ${myTable} WHERE age > ?`,
[18]
);
// Single row
const user = await client.database.queryOne<User>(
`SELECT * FROM ${myTable} WHERE id = ?`,
[1]
);// INSERT, UPDATE, DELETE
const result = await client.database.execute(
`INSERT INTO ${myTable} (name) VALUES (?)`,
['Alice']
);
console.log(result.lastInsertId);
console.log(result.rowsAffected);await client.database.transaction([
`INSERT INTO ${myTable} (name) VALUES ('Alice')`,
`INSERT INTO ${myTable} (name) VALUES ('Bob')`
]);// Create table
await client.database.createTable('posts', `
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT
`);
// Check existence
const exists = await client.database.tableExists(myTable);
// Get table info
const info = await client.database.getTableInfo(myTable);
// List all tables
const tables = await client.database.listTables();
// Drop table
await client.database.dropTable('posts');
// Insert
const id = await client.database.insert(myTable, {
name: 'John',
email: '[email protected]'
});
// Update
const updated = await client.database.update(
myTable,
{ name: 'Jane' },
'id = ?',
[id]
);
// Delete
const deleted = await client.database.delete(myTable, 'id = ?', [id]);
// Count
const count = await client.database.count(myTable, 'age > ?', [18]);// Store data
await client.storage.setItem('theme', 'dark');
// Retrieve data
const theme = await client.storage.getItem('theme');
// Remove data
await client.storage.removeItem('theme');
// Get all keys
const keys = await client.storage.keys();
// Clear all
await client.storage.clear();// Get all dependencies
const deps = await client.getDependencies();
// Each dependency has:
// {
// publicKey: string,
// name: string,
// version: string
// }// Request permission (runtime - usually done via manifest)
const response = await client.requestDatabasePermission({
resource: 'MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k__password-manager__credentials',
operation: 'read',
reason: 'To retrieve email credentials'
});
if (response.status === 'granted') {
// Permission granted!
}
// Check if permission exists
const hasPermission = await client.checkDatabasePermission(
'MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k__password-manager__credentials',
'read'
);// Listen to context changes
client.on('context.changed', (event) => {
console.log('Context changed:', event.data.context);
});
// Listen to search requests
client.on('search.request', (event) => {
const { query, requestId } = event.data;
// Respond with search results
await client.respondToSearch(requestId, [
{
id: '1',
title: 'Result 1',
description: 'Description',
type: 'item',
score: 0.9
}
]);
});
// Remove listener
const callback = (event) => console.log(event);
client.on('some-event', callback);
client.off('some-event', callback);// Clean up when extension is destroyed
client.destroy();Your extension needs a manifest.json file:
{
"name": "my-extension",
"version": "1.0.0",
"description": "My awesome extension",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...",
"signature": "...",
"namespace": "johndoe",
"displayName": "My Extension",
"author": "John Doe <[email protected]>",
"icon": "icon.png",
"main": "index.html",
"permissions": ["http.fetch", "notifications.show"],
"dependencies": [
{
"publicKey": "MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k",
"name": "password-manager",
"minVersion": "1.0.0",
"reason": "To access stored credentials",
"tables": [
{
"table": "credentials",
"operations": ["read"],
"reason": "Read email login credentials"
}
]
}
]
}import { useHaexHub } from '@haexhub/sdk/vue';
const { db, getTableName } = useHaexHub();
// Create credentials table - no permissions needed for own tables!
const credentialsTable = getTableName('credentials');
await db.createTable(credentialsTable, `
id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
`);
// Store credentials - full access to own tables
await db.insert(credentialsTable, {
service: 'gmail',
username: '[email protected]',
password: 'encrypted_password'
});manifest.json - Must declare dependency and request permission:
{
"name": "email-client",
"version": "1.0.0",
"dependencies": [
{
"publicKey": "MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k",
"name": "password-manager",
"minVersion": "1.0.0",
"reason": "Access stored credentials for email sync",
"tables": [
{
"table": "credentials",
"operations": ["read"],
"reason": "Retrieve Gmail login credentials"
}
]
}
]
}Code:
import { useHaexHub } from '@haexhub/sdk/react';
function EmailClient() {
const { db, client } = useHaexHub();
async function loadCredentials() {
// Access Password Manager's credentials table
// ✅ Works because we declared permission in manifest
const credentialsTable = client.getDependencyTableName(
'MCowBQYDK2VwAyEAp1q2r3s4t5u6v7w8x9y0z1a2b3c4d5e6f7g8h9i0j1k', // Password Manager's publicKey
'password-manager', // Extension name
'credentials' // Table name
);
// Read Gmail credentials (read permission granted via manifest)
const creds = await db.queryOne(
`SELECT username, password FROM ${credentialsTable} WHERE service = ?`,
['gmail']
);
if (creds) {
connectToGmail(creds.username, creds.password);
}
}
return <button onClick={loadCredentials}>Connect Gmail</button>;
}User Experience:
- User installs Email Client extension
- HaexHub shows permission request: "Email Client wants to read the credentials table from Password Manager"
- User sees the reason: "Retrieve Gmail login credentials"
- User approves or denies
- Permission can be revoked anytime in settings
HaexHub Extensions must be cryptographically signed to ensure authenticity and prevent tampering. The SDK provides tools to generate keypairs, sign, and package your extensions.
Before publishing your extension, generate a keypair:
npx haexhub keygenThis creates two files:
public.key- Include this in your repositoryprivate.key- Keep this secret! Add to.gitignore
Important: Never commit your private.key. Anyone with this key can impersonate your extension.
private.key
*.key
!public.keyAdd scripts to your package.json:
{
"scripts": {
"build": "nuxt generate",
"package": "haexhub sign dist -k private.key",
"build:release": "npm run build && npm run package"
},
"devDependencies": {
"@haexhub/sdk": "^0.1.0"
}
}Then build and package:
npm run build:releaseThis creates your-extension-1.0.0.haextension - a signed ZIP file ready for distribution.
OR build your extension and run:
npx haexhub sign dist -k private.keyThe signing process:
- Computes SHA-256 hash of all files in your extension
- Signs the hash with your private key using Ed25519
- Adds
public_keyandsignatureto yourmanifest.json - Creates a
.haextensionfile (ZIP archive)
When users install your extension:
- HaexHub extracts the
.haextensionfile - Verifies the signature using the
public_key - Computes the hash and checks it matches
- Rejects installation if verification fails
This ensures the extension hasn't been modified since you signed it.
- Backup your private key - Store it securely (password manager, encrypted backup)
- One key per extension - Don't reuse keys across different extensions
- Rotate keys carefully - Key changes require users to reinstall your extension
- Lost key = lost extension - You cannot update an extension without the original key
- Own tables: Full CRUD access without permissions
- Table namespacing: Automatic prefix with publicKey prevents conflicts
- Sandbox isolation: Extensions cannot access each other's data by default
- No manifest bloat: No need to declare own tables
- Each extension identified by public key hash
- Impossible to impersonate another extension
- Works across all registries
- Tamper-proof signing with Ed25519
- Per-table permissions: Request access to specific tables only
- Per-operation control:
readand/orwriteper table - Explicit manifest declarations: Dependencies must be declared upfront
- User consent required: All cross-extension access needs approval
- Human-readable reasons: Users see why permission is needed
- Must declare dependencies in manifest
- Extension name must match publicKey
- Version requirements enforced (semver)
- Missing dependencies prevent installation
- Every database access validated in real-time
- Permission checks on every query
- User can revoke permissions anytime
- No way to bypass permission system
- Audit trail for all cross-extension access
Full TypeScript support included:
import type {
ExtensionInfo,
ApplicationContext,
DatabaseQueryResult,
PermissionStatus
} from '@haexhub/sdk';import { ErrorCode } from '@haexhub/sdk';
try {
await client.database.query(`SELECT * FROM ${someTable}`);
} catch (error) {
if (error.code === ErrorCode.PERMISSION_DENIED) {
console.error('Permission denied');
} else if (error.code === ErrorCode.TIMEOUT) {
console.error('Request timeout');
} else {
console.error('Error:', error.message);
}
}# Install dependencies
pnpm install
# Build the SDK
pnpm build
# Watch mode for development
pnpm dev
# Link locally for testing
pnpm linkCreate a new release using the automated scripts:
# Patch release (1.2.3 → 1.2.4)
pnpm release:patch
# Minor release (1.2.3 → 1.3.0)
pnpm release:minor
# Major release (1.2.3 → 2.0.0)
pnpm release:majorThe script automatically:
- Updates version in
package.json - Creates a git commit
- Creates a git tag
- Pushes to remote
After the release, publish to npm:
pnpm publishVersionISC
- Documentation: https://github.com/haexhub/sdk
- GitHub: https://github.com/haexhub/sdk
- Issues: https://github.com/haexhub/sdk/issues