Back to docs

SDK Full Reference

Exhaustive reference of all BizzOptima Module SDK APIs — types, capabilities, and real implementation examples.

Reference for @bizzoptima/module-sdk v1. Each section includes the full method signatures, required capability, and a complete real-world implementation example.

Trust levels: unsigned_local → signed_local → signed_registry → trusted_bundled / dev_bypass.
SDK 2.0 capabilities require at minimum signed_local. Unsigned modules only have db:read and events:subscribe.

Core namespaces — always available

api.db — SQLite databasedb:read / db:write

Local SQLite database scoped to the module. Other modules' tables are not accessible.

db.querydb:read
<T>(sql: string, params?: unknown[]) => Promise<T[]>

SELECT queries. Positional params with ?.

db.rundb:write
(sql: string, params?: unknown[]) => Promise<{ changes: number; lastInsertRowid: number | bigint }>

Real example — Contact management module

tsx
interface Contact { id: string; name: string; phone: string; created_at: string; }

// In init() — DB migration
async function migrate(api: BizzOptimaModuleAPI) {
  await api.db.run(`
    CREATE TABLE IF NOT EXISTS contacts (
      id TEXT PRIMARY KEY, name TEXT NOT NULL,
      phone TEXT, email TEXT, created_at TEXT NOT NULL
    )
  `);
}

// React component with search, add, delete
function ContactList() {
  const api = useModuleAPI();
  const toast = useToast();
  const [search, setSearch] = React.useState('');

  const { data: contacts, loading, refetch } = useDB<Contact>(
    'SELECT * FROM contacts WHERE name LIKE ? ORDER BY name ASC',
    ['%' + search + '%'],
  );

  async function deleteContact(id: string) {
    const ok = await api.ui.confirm('Delete this contact?');
    if (!ok) return;
    const result = await api.db.run('DELETE FROM contacts WHERE id = ?', [id]);
    if (result.changes > 0) { toast('Contact deleted', { type: 'info' }); refetch(); }
  }

  async function addContact(name: string, phone: string) {
    await api.db.run(
      'INSERT INTO contacts (id, name, phone, email, created_at) VALUES (?,?,?,?,?)',
      [crypto.randomUUID(), name, phone, '', new Date().toISOString()],
    );
    toast('Contact added', { type: 'success' });
    refetch();
  }

  if (loading) return <div>Loading…</div>;
  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search…" />
      {contacts.map(c => (
        <div key={c.id}>
          <strong>{c.name}</strong> — {c.phone}
          <button onClick={() => deleteContact(c.id)}>Delete</button>
        </div>
      ))}
      <button onClick={() => addContact('New Contact', '+1 555 000 0000')}>Add</button>
    </div>
  );
}
✅ v0.1.0
api.events — Inter-module busevents:emit / events:subscribe

Shared event bus across all active modules. Lets a module react to another module's actions without direct coupling.

events.emitevents:emit
(name: string, payload: unknown, options?) => Promise<void>
events.subscribeevents:subscribe
(name: string, callback) => () => void

Returned function unsubscribes the listener.

Real example — Loyalty module reacts to sales

tsx
// In init() of loyalty module — subscribe to sales events
function initLoyalty(api: BizzOptimaModuleAPI) {
  api.db.run(`
    CREATE TABLE IF NOT EXISTS loyalty_points (
      customer_id TEXT PRIMARY KEY, points INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL
    )
  `);

  return api.events.subscribe('sale.completed', async (raw: unknown) => {
    const { customerId, total } = raw as { customerId: string | null; total: number };
    if (!customerId) return;

    const pointsEarned = Math.floor(total / 10); // 1 point per $10
    if (!pointsEarned) return;

    await api.db.run(`
      INSERT INTO loyalty_points (customer_id, points, updated_at) VALUES (?, ?, ?)
      ON CONFLICT(customer_id) DO UPDATE SET
        points = points + excluded.points, updated_at = excluded.updated_at
    `, [customerId, pointsEarned, new Date().toISOString()]);

    api.ui.toast('+' + pointsEarned + ' loyalty point(s) awarded!', { type: 'success' });
  });
}

// In sales module — emit on each completed sale
async function completeSale(api: BizzOptimaModuleAPI, sale: Sale) {
  // ... save to DB ...
  await api.events.emit('sale.completed', {
    saleId: sale.id, customerId: sale.customerId,
    total: sale.total, currency: api.config.getCurrency(),
  }, { entityType: 'sale', entityId: sale.id });
}
⚠️ Partiel v0.1.0
api.config — Business configurationAlways available

Read-only snapshot of the business configuration set up at first launch.

config.getBusiness
() => BusinessConfigSnapshot

{ businessId, businessName, currency, locale, activeModuleIds… }

config.getCurrency
() => string

ISO 4217: 'USD', 'EUR', 'XAF'…

config.getLocale
() => 'fr' | 'en'
config.getInstalledModules
() => Promise<InstalledModuleInfo[]>

Includes trustLevel per module.

Real example — Multi-currency display + module status

tsx
function PriceDisplay({ amount }: { amount: number }) {
  const locale = useLocale();
  const currency = useCurrency();
  const formatted = new Intl.NumberFormat(locale === 'fr' ? 'fr-FR' : 'en-US', {
    style: 'currency', currency, minimumFractionDigits: 0,
  }).format(amount);
  return <span className="font-mono font-semibold">{formatted}</span>;
}

function ModuleStatus() {
  const business = useBusinessConfig();
  const hasInventory = business.activeModuleIds.includes('inventory');
  return (
    <div>
      <h3>{business.businessName}</h3>
      <p>Currency: {business.currency} · Locale: {business.locale}</p>
      {!hasInventory && <div className="warning">Inventory module not active.</div>}
    </div>
  );
}
⚠️ Partiel v0.1.0
api.ui — UI helpersAlways available

Toasts, confirmations and alerts rendered in BizzOptima's main UI.

ui.toast
(message: string, options?) => void

options: { type, duration }

ui.confirm
(message: string, title?) => Promise<boolean>
ui.alert
(message: string, title?) => void

Real example — Delete with confirmation

tsx
async function deleteInvoice(api: BizzOptimaModuleAPI, invoiceId: string, number: string) {
  const confirmed = await api.ui.confirm(
    'Delete invoice #' + number + '? This action cannot be undone.',
    'Confirm deletion',
  );
  if (!confirmed) return;

  const payments = await api.db.query(
    'SELECT id FROM payments WHERE invoice_id = ? AND status = ?',
    [invoiceId, 'completed'],
  );
  if (payments.length > 0) {
    await api.ui.alert(payments.length + ' completed payment(s) linked — cannot delete.', 'Blocked');
    return;
  }

  await api.db.run('DELETE FROM invoice_lines WHERE invoice_id = ?', [invoiceId]);
  await api.db.run('DELETE FROM invoices WHERE id = ?', [invoiceId]);
  await api.events.emit('invoice.deleted', { invoiceId, number });
  api.ui.toast('Invoice #' + number + ' deleted', { type: 'success' });
}
⚠️ Partiel v0.1.0
api.interModule — Secure communicationAlways available

Typed RPC between active modules with caller restriction. The caller has no access to internal implementation.

interModule.expose
(method, handler, options?) => () => void

options.allowedCallers: moduleId[] restricts access.

interModule.request
<T>(targetId, method, params?) => Promise<T>

Real example — Inventory exposes stock check to Cashier

tsx
// inventory module init():
api.interModule.expose('checkStock', async (params: unknown) => {
  const { productId, qty = 1 } = params as { productId: string; qty?: number };
  const [row] = await api.db.query<{ available: number; reserved: number }>(
    'SELECT available, reserved FROM stock WHERE product_id = ?', [productId],
  );
  const s = row ?? { available: 0, reserved: 0 };
  return { productId, available: s.available, canSell: s.available - s.reserved >= qty };
});

api.interModule.expose('reserveStock',
  createSecureHandler(async (params: unknown) => {
    const { productId, qty } = params as { productId: string; qty: number };
    await api.db.run('UPDATE stock SET reserved = reserved + ? WHERE product_id = ?', [qty, productId]);
    return { ok: true };
  }, { moduleId: 'inventory', allowedCallers: ['quick_cashier', 'sales'] }),
);

// quick_cashier module — using inventory:
async function addToCart(api: BizzOptimaModuleAPI, productId: string, qty: number) {
  const stock = await api.interModule.request<{ canSell: boolean; available: number }>(
    'inventory', 'checkStock', { productId, qty },
  );
  if (!stock.canSell) {
    api.ui.toast('Insufficient stock: ' + stock.available + ' available', { type: 'warning' });
    return;
  }
  await api.interModule.request('inventory', 'reserveStock', { productId, qty });
  await api.db.run('INSERT INTO cart_items (product_id, qty) VALUES (?,?)', [productId, qty]);
}
⚠️ Partiel v0.1.0
api.security — Security contextAlways available

Read-only trust level and capabilities, available from initialization.

security.getTrustLevel
() => ModuleTrustLevel
security.hasCapability
(cap: ModuleCapability) => boolean
security.requireCapability
(cap: ModuleCapability) => void

Throws CapabilityError if missing.

Real example — Feature gating by signature level

tsx
import { usePermission, PermissionGuard, getMissingCapabilities, CapabilityError } from '@bizzoptima/module-sdk';

function SyncPanel() {
  const api = useModuleAPI();
  const trustLevel = useTrustLevel();

  if (trustLevel === 'unsigned_local') {
    return (
      <div className="upgrade-banner">
        <p>This module is not signed. Cloud sync requires at least signed_local.</p>
        <a href="https://bizzoptima.com/docs/module-sdk#signing">Sign my module →</a>
      </div>
    );
  }

  return (
    <PermissionGuard api={api} requires={['http:request', 'fs:write']}
      fallback={
        <p>Missing: {getMissingCapabilities(api, ['http:request', 'fs:write']).join(', ')}</p>
      }
    >
      <ExportButton />
    </PermissionGuard>
  );
}

async function exportAndSync(api: BizzOptimaModuleAPI) {
  try {
    api.security.requireCapability('fs:write');
    api.security.requireCapability('http:request');
  } catch (err) {
    if (err instanceof CapabilityError) {
      api.ui.alert('Missing capability: ' + err.capability + '. Sign the module to unlock.');
      return;
    }
    throw err;
  }
  const data = await api.db.query('SELECT * FROM sales WHERE synced = 0');
  await api.fs.writeText('export/unsynced.json', JSON.stringify(data));
  await api.http.post('https://erp.example.com/api/sync', data);
}
⚠️ Partiel v0.1.0
api.settings — Persisted settingssettings:read / settings:write

Key/value storage scoped to the module. Independent from SQLite.

settings.getsettings:read
(key) => Promise<string | null>
settings.setsettings:write
(key, value) => Promise<void>
settings.deletesettings:write
(key) => Promise<void>
settings.getAllsettings:read
() => Promise<Record<string, string>>
tsx
const [erpUrl, setErpUrl] = useSetting('erp_url', '');
// Imperative: await api.settings.set('last_sync', new Date().toISOString());
❌ v0.2.0Not yet in the preload bridge
api.navigatenavigate
navigatenavigate
(route: string) => void
tsx
const navigate = useNavigate();
navigate('/modules/inventory');
⚠️ Via renderer callback

SDK 2.0 namespaces — require signed module

api.http — CORS-free HTTPhttp:request

All requests go through the Electron main process — no CORS, follows system proxy, supports self-signed HTTPS.

http.get
<T>(url: string, headers?: Record<string,string>) => Promise<T>
http.post
<T>(url: string, body: unknown, headers?: Record<string,string>) => Promise<T>
http.request
(url: string, init?: HttpRequestInit) => Promise<HttpResponse>

Real example — ERP sync with error handling

tsx
interface ErpProduct { sku: string; name: string; price: number; stock: number; }

async function syncFromErp(api: BizzOptimaModuleAPI, erpUrl: string, apiKey: string) {
  api.security.requireCapability('http:request');

  const products = await api.http.get<ErpProduct[]>(
    erpUrl + '/api/products',
    { 'X-API-Key': apiKey },
  );

  let imported = 0;
  for (const p of products) {
    await api.db.run(`
      INSERT INTO products (sku, name, price, stock, synced_at) VALUES (?, ?, ?, ?, ?)
      ON CONFLICT(sku) DO UPDATE SET
        name = excluded.name, price = excluded.price,
        stock = excluded.stock, synced_at = excluded.synced_at
    `, [p.sku, p.name, p.price, p.stock, new Date().toISOString()]);
    imported++;
  }

  await api.settings.set('last_erp_sync', new Date().toISOString());
  return imported;
}

// Hook for simple fetches
function ExchangeRates() {
  const { data: rates, loading } = useHttp<{ USD: number }>(
    'https://api.exchangerate.host/latest?base=XAF&symbols=USD',
  );
  if (loading) return <p>Loading rates…</p>;
  return <p>1 XAF = {rates?.USD?.toFixed(6)} USD</p>;
}
✅ v0.1.0
api.fs — Sandboxed file systemfs:read / fs:write / fs:dialog

Access restricted to userData/module-data/{moduleId}/. Module isolation is enforced.

fs.readText / writeText
(path: string, content?: string) => Promise<string|null|void>
fs.list
(directory?: string) => Promise<FsFileInfo[]>
fs.exists / delete
(path: string) => Promise<boolean|void>
fs.pickFilefs:dialog
(filters?) => Promise<string|null>

Real example — Export sales to CSV

tsx
function escCsv(val: string | number): string {
  const s = String(val);
  return (s.includes(',') || s.includes('"')) ? '"' + s.replace(/"/g, '""') + '"' : s;
}

async function exportSalesToCsv(api: BizzOptimaModuleAPI, from: string, to: string) {
  api.security.requireCapability('fs:write');

  const sales = await api.db.query<{
    id: string; date: string; customer_name: string; total: number;
  }>(`
    SELECT s.id, s.created_at as date, c.name as customer_name, s.total
    FROM sales s LEFT JOIN customers c ON s.customer_id = c.id
    WHERE s.created_at BETWEEN ? AND ? ORDER BY s.created_at DESC
  `, [from, to]);

  if (!sales.length) { api.ui.alert('No sales in selected period.'); return; }

  const csv = ['ID,Date,Customer,Total',
    ...sales.map(s => [escCsv(s.id), escCsv(s.date.slice(0,10)),
      escCsv(s.customer_name ?? 'Anonymous'), escCsv(s.total)].join(','))
  ].join('\n');

  await api.fs.writeText('exports/sales_' + from.slice(0,10) + '.csv', csv);
  api.ui.toast('Export created: ' + sales.length + ' rows', { type: 'success' });
}
✅ v0.1.0
api.scheduler — Job schedulerscheduler:manage

SQLite-persisted jobs that survive restarts. Tasks fire even if the module was inactive.

scheduler.once / interval / daily
(id, delay/ms/time, callback) => ScheduledJob
scheduler.cancel
(id) => void
scheduler.onFired
(callback) => () => void

Real example — Daily report + stock alert + startup backup

tsx
function initScheduler(api: BizzOptimaModuleAPI) {
  // Daily sales report at 11 PM
  api.scheduler.daily('daily-report', '23:00', async () => {
    const today = new Date().toISOString().slice(0, 10);
    const [s] = await api.db.query<{ total: number; count: number }>(
      'SELECT SUM(total) as total, COUNT(*) as count FROM sales WHERE DATE(created_at) = ?', [today],
    );
    await api.fs.writeText('reports/' + today + '.json', JSON.stringify(s));
    api.notifications.send('Daily report ready', s?.count + ' sale(s) today');
  });

  // Stock alert every 2 hours
  api.scheduler.interval('stock-check', 2 * 60 * 60 * 1000, async () => {
    const low = await api.db.query<{ name: string; available: number }>(
      'SELECT name, available FROM products WHERE available <= min_stock AND min_stock > 0',
    );
    if (low.length > 0) {
      api.notifications.send(low.length + ' product(s) low',
        low.slice(0,3).map(p => p.name + ': ' + p.available + ' left').join('\n'));
    }
  });

  // Auto-backup 5 min after start
  api.scheduler.once('startup-backup', 5 * 60 * 1000, async () => {
    const data = await api.db.query('SELECT * FROM sales ORDER BY created_at DESC LIMIT 1000');
    await api.fs.writeText('backups/latest.json', JSON.stringify(data));
  });
}
⚠️ Partiel v0.1.0
api.print · api.notifications · api.clipboard · api.shell · api.network · api.agentVarious

print — Generate & print an invoice (HTML)

tsx
async function printInvoice(api: BizzOptimaModuleAPI, invoiceId: string) {
  api.security.requireCapability('print:document');
  const [inv] = await api.db.query<{ number: string; customer: string; total: number }>(
    'SELECT i.number, c.name as customer, i.total FROM invoices i JOIN customers c ON i.customer_id = c.id WHERE i.id = ?',
    [invoiceId],
  );
  if (!inv) return;
  const html = `<!DOCTYPE html><html><head><style>
    body{font-family:Arial} .total{color:#059669;font-size:20px;font-weight:bold}
  </style></head><body>
    <h1>${api.config.getBusiness().businessName}</h1>
    <p>Invoice #${inv.number} — ${inv.customer}</p>
    <p class="total">Total: ${inv.total.toLocaleString()} ${api.config.getCurrency()}</p>
  </body></html>`;
  await api.print.html(html);
  api.ui.toast('Invoice sent to printer', { type: 'success' });
}

agent — AI sales analysis

tsx
async function askAI(api: BizzOptimaModuleAPI, question: string) {
  if (!api.agent.isAvailable()) return 'Xpress AI not configured.';

  const result = await api.agent.run({
    systemPrompt: 'You are a sales analyst. Use tools for up-to-date data.',
    prompt: question,
    maxSteps: 5,
    tools: [{
      name: 'sales_summary',
      description: 'Get sales total and count for a date range',
      parameters: { from_date: { type: 'string' }, to_date: { type: 'string' } },
      handler: async ({ from_date, to_date }) => api.db.query(
        'SELECT COUNT(*) as count, SUM(total) as total FROM sales WHERE DATE(created_at) BETWEEN ? AND ?',
        [from_date, to_date],
      ),
    }],
  });

  return result.output;
}

clipboard · shell · network

tsx
// Copy reference to clipboard
await api.clipboard.writeText(txId);

// Open generated file in default app
api.shell.openPath('exports/report.csv');

// Offline-aware rendering
function OfflineBanner() {
  const { online } = useNetwork();
  if (online) return null;
  return <div style={{ background: '#fef2f2' }}>⚠️ Offline — cloud sync paused.</div>;
}
✅ v0.1.0

React Hooks

Available hooks
useModuleAPI
() => BizzOptimaModuleAPI

Access the full API instance.

useBusinessConfig
() => BusinessConfigSnapshot
useLocale
() => 'fr' | 'en'
useCurrency
() => string
useDB
<T>(sql, params?) => { data, loading, error, refetch }
useModuleEvents
(eventName, callback) => void

Auto-unsubscribes on unmount.

useToast
() => (message, options?) => void
useNavigate
() => (route) => void
useSecurity
() => SecurityContext
useTrustLevel
() => ModuleTrustLevel
usePermission
(cap: ModuleCapability) => boolean
usePermissions
(caps: ModuleCapability[]) => boolean

Returns true if ALL capabilities are available.

useNetwork
() => { online: boolean }
useHttp
<T>(url, init?) => { data, loading, error, refetch }

Requires http:request.

useSetting
(key, defaultValue?) => [value, setter, loading]

Requires settings:read/write.

Security utilities

Guards and helpers
PermissionGuard
({ api, requires, fallback?, children }) => JSX

Conditionally renders based on capability.

createSecureHandler
(handler, options) => SecureHandler

Wraps an inter-module handler with caller verification.

createPaymentGuard
(api, { allowedCallers? }) => (caller?) => void

Guard for sensitive payment operations.

assertCapabilities
(api, capabilities[]) => void

Throws CapabilityError on first missing capability.

getMissingCapabilities
(api, required[]) => ModuleCapability[]
validateManifest
(manifest) => { valid, errors[], warnings[] }

Capabilities by trust level

Capabilityunsignedsigned_localsigned_registrytrusted_bundled
db:read✅✅✅✅
db:write—✅✅✅
events:subscribe✅✅✅✅
events:emit—✅✅✅
http:request—✅✅✅
fs:read / fs:write—✅✅✅
fs:dialog——✅✅
scheduler:manage—✅✅✅
notifications:send—✅✅✅
print:document——✅✅
clipboard:write—✅✅✅
clipboard:read——✅✅
shell:open—✅✅✅
settings:read——✅✅
settings:write———✅