PSA/ee/docs/extension-system/development_guide.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

14 KiB

Alga PSA Extension Development Guide (Runner + Iframe UI)

This guide describes how to build Enterprise Edition (v2) extensions for Alga PSA:

  • Server-side handlers execute out-of-process in the Runner as Wasmtime components produced by componentize-js
  • UI is rendered exclusively in sandboxed iframes
  • Bundles are signed and content-addressed (sha256:...) and validated by the Registry

Core rules: See “Correctness Rules” in the README for the canonical list. Reference gateway scaffold: server/src/app/api/ext/[extensionId]/[[...path]]/route.ts

Prerequisites

  • Node.js 18+
  • Component toolchain:
    • @bytecodealliance/componentize-js (jco) or another WIT-compatible pipeline
    • @alga-psa/extension-runtime helpers for handlers/tests
  • Familiarity with:
    • TypeScript, React (for UI)
    • Security best practices and least-privilege design

SDK and CLI Tools

The Alga SDK provides CLI tools and libraries to streamline extension development:

Installation

npm install -g @alga-psa/cli
# Or use npx: npx @alga-psa/cli <command>

CLI Commands

Command Description
alga create extension <name> Scaffold a new extension project
alga create component <name> Scaffold a WASM component project
alga build Build the extension (compile TS → WASM if needed)
alga pack Create bundle.tar.zst from built extension
alga extension publish <dir> Build, pack, and publish to an Alga instance
alga extension install <id> Install an extension from the registry
alga extension uninstall <id> Uninstall an extension
alga sign <bundle> Sign a bundle with cosign, x509, or pgp

SDK Packages

Package Purpose
@alga-psa/cli Command-line tools for building and publishing
@alga-psa/client-sdk Programmatic APIs for build/pack/publish
@alga-psa/extension-runtime TypeScript types and helpers for WASM handlers
@alga/extension-iframe-sdk Host communication APIs for iframe UIs

Project Layout (Example)

my-extension/
├── src/
│   ├── component/                # Component handler source (TS/JS + @alga-psa/extension-runtime)
│   │   ├── handler.ts
│   │   └── wit/                  # Generated WIT/world bindings
│   ├── ui/                       # Iframe app (Vite + React)
│   │   ├── index.html
│   │   └── src/
│   │       ├── main.tsx
│   │       └── components/
│   └── manifest.json             # Manifest v2
├── dist/
│   ├── main.wasm                 # Componentized artifact produced by jco componentize
│   └── ui/                       # Built iframe assets
├── SIGNATURE                     # Signature file (generated by CI)
├── package.json
└── README.md

Step-by-step: Build + Package an Extension

Quick Start with CLI

The fastest way to create and build an extension:

# Create a new extension project
alga create extension my-extension
cd my-extension

# Build (compiles TypeScript and creates WASM if needed)
alga build

# Create the bundle archive
alga pack
# Output: dist/bundle.tar.zst

# Publish to your Alga instance
alga extension publish . --api-key $ALGA_API_KEY --tenant $ALGA_TENANT_ID

Manual Setup

  1. Scaffold directories

    • Create component/ for your handler sources (e.g., component/src/handler.ts) and copy the platform WIT definitions into component/wit/extension-runner.wit.
    • Add ui/ for your static iframe app (any bundler). The host will serve files from ui/dist inside the bundle.
    • Place manifest.json at the repo root alongside package.json.
  2. Author the Wasmtime endpoint

    • Install the toolchain: npm install @alga-psa/extension-runtime @bytecodealliance/componentize-js.
    • Implement handler using the runtime helpers:
      // component/src/handler.ts
      import { Handler, jsonResponse } from '@alga-psa/extension-runtime';
      
      export const handler: Handler = async (req, host) => {
        const secret = await host.secrets.get('api_key');
        const upstream = await host.http.fetch({
          url: 'https://httpbin.org/get',
          headers: { authorization: `Bearer ${secret}` },
        });
        return jsonResponse({ ok: true, upstream: upstream.status });
      };
      
    • Compile to JavaScript (e.g., tsc or Vite build) so dist/js/handler.js exists.
    • Run jco componentize to produce the Wasmtime component:
      npx jco componentize dist/js/handler.js --wit ./wit/extension-runner.wit --world-name runner --out dist/main.wasm
  3. Add static UI assets

    • Build your iframe application into ui/dist (for example, npm run ui:build producing ui/dist/index.html, JS/CSS chunks, images).
    • Reference the entry file from the manifest via "ui": { "type": "iframe", "entry": "ui/dist/index.html" }.
  4. Create or update manifest.json

    • Include metadata, capabilities, and assets globs:
      {
        "name": "com.example.basic",
        "publisher": "Example Inc.",
        "version": "1.0.0",
        "runtime": "wasm-js@1",
        "capabilities": ["cap:http.fetch", "cap:secrets.get", "cap:log.emit", "cap:ui.proxy"],
        "ui": { "type": "iframe", "entry": "ui/dist/index.html" },
        "assets": ["ui/dist/**/*"]
      }
      
    • Note: api.endpoints is optional advisory metadata for documentation/tooling. It is NOT required for the proxy pattern to work.
  5. Package the bundle

    Using the CLI (recommended):

    alga pack --project .
    # Creates dist/bundle.tar.zst with proper structure
    

    Manual packaging:

    • Layout your release directory:
      dist/main.wasm
      ui/dist/**/*
      manifest.json
      precompiled/ (optional cwasm artifacts)
      
    • Create the canonical tarball (zstd recommended): tar --zstd -cf bundle.tar.zst manifest.json dist/main.wasm ui/dist
    • Compute the content hash: shasum -a 256 bundle.tar.zstsha256:...
    • Produce SIGNATURE (detached) with your publisher key per security_signing.md.
  6. Publish & install

    • Upload bundle.tar.zst, manifest.json, SIGNATURE, and metadata via your CI/registry flow.
    • During tenant installation, operators provide install-scoped config and secrets that the gateway will pass to the runner.

With these steps, the Runner can fetch your component, serve the static UI, and execute the Wasmtime handler when /api/ext/{extensionId}/... is invoked.

Manifest v2 (abridged)

See Manifest Schema for full details.

{
  "name": "com.acme.my-extension",
  "publisher": "Acme Inc.",
  "version": "1.0.0",
  "runtime": "wasm-js@1",
  "capabilities": ["cap:http.fetch", "cap:storage.kv", "cap:secrets.get", "cap:ui.proxy"],
  "ui": { "type": "iframe", "entry": "ui/index.html" },
  "assets": ["ui/**/*"]
}

Key points:

  • ui.entry points to your iframe HTML within the bundle.
  • cap:ui.proxy enables the postMessage proxy pattern for UI→WASM handler communication.
  • capabilities request access to host features (http, storage, secrets, ui proxy, logging) and are granted per tenant install.
  • api.endpoints is optional advisory metadata for documentation/tooling—not required for the extension to function.

Building Server Handlers (Componentized WASM)

  • Write handlers in TypeScript (or JS) using @alga-psa/extension-runtime, then run jco componentize to produce dist/main.wasm.
  • Optional: use other WIT-compatible languages (wit-bindgen for Rust/TinyGo) as long as the output conforms to the alga:extension/runner world.
  • Use host APIs (capability-scoped) exposed via the generated bindings:
    • host.http.fetch, host.storage.*, host.secrets.get, host.uiProxy.callRoute, host.logging.*.
    • host.invoicing.createManualInvoice (requires cap:invoice.manual.create) for creating draft manual invoices.

Conceptual handler shape:

import { Handler, jsonResponse } from '@alga-psa/extension-runtime';

export const handler: Handler = async (request, host) => {
  const items = await host.storage.list({
    namespace: 'agreements',
    limit: 50,
  });
  return jsonResponse(
    { data: items },
    { headers: { 'content-type': 'application/json' } }
  );
};

Build example:

Using the CLI:

alga build --project .
# Automatically detects if WASM build is needed based on manifest
# Outputs: dist/main.wasm

Using npm scripts:

npm run build          # compiles TS → JS
npm run build:component
# scripts should run:
# jco componentize dist/js/handler.js --wit ./wit/extension-runner.wit \
#   --world-name runner --out dist/main.wasm

CI should output dist/main.wasm plus optional precompiled/ artifacts referenced by the manifest.

Building the Iframe UI

  • Use the provided SDKs:
    • @alga/extension-iframe-sdk for host communication (auth, theme, navigation, proxy calls)
    • @alga/ui-kit for accessible, consistent components
  • Iframe bootstrap and URL construction are handled by the host via:
  • UI assets are served by the Runner at ${RUNNER_PUBLIC_BASE}/ext-ui/{extensionId}/{content_hash}/[...] with immutable caching

Calling Your WASM Handler from the UI (postMessage Proxy Pattern)

Important: Extension UIs should NOT make direct fetch() calls to /api/ext/ endpoints. Instead, use the postMessage proxy pattern which ensures:

  • Authentication is handled by the host
  • Secrets never reach the browser
  • Consistent error handling and timeouts

Using the iframe SDK:

import { IframeBridge } from '@alga/extension-iframe-sdk';

const bridge = new IframeBridge();
await bridge.ready();

// Call your WASM handler via the proxy
const response = await bridge.uiProxy.call('/agreements');
const data = JSON.parse(new TextDecoder().decode(response));

Or using the lower-level postMessage API (see client-portal-test sample):

// Send request to host
window.parent.postMessage({
  alga: true,
  version: '1',
  type: 'apiproxy',
  request_id: crypto.randomUUID(),
  payload: { route: '/agreements' }
}, '*');

// Listen for response
window.addEventListener('message', (ev) => {
  if (ev.data?.type === 'apiproxy_response') {
    // Handle response...
  }
});

The cap:ui.proxy capability is required in your manifest to use the proxy pattern.

Signing and Packaging

  • Produce a content-addressed bundle including:
    • manifest.json
    • WASM artifacts under dist/
    • UI assets under ui/
    • Optional precompiled artifacts (cwasm) under precompiled/
  • Generate a SHA256 for the canonical bundle; sign with your publisher certificate; include SIGNATURE
  • CI outputs an artifact ready for publish (e.g., bundle.tar.zst)

See Security & Signing.

Publish and Install

  • Publish the bundle and metadata to the Registry
  • Admin installs a version for a tenant and grants capabilities
  • The app server does not store or execute tenant code; bundles are verified and referenced via content_hash

Calling Your Handlers (Gateway)

  • Host exposes /api/ext/[extensionId]/[[...path]].
  • Gateway resolves install → version/content hash → config/providers/sealed secrets and calls Runner POST /v1/execute with that metadata.
  • Header/time/size policies are enforced by the Gateway and Runner.
  • Manifest endpoint lists are used for documentation/UX; strict enforcement is under active discussion (see plan).

Reference route scaffold: server/src/app/api/ext/[extensionId]/[[...path]]/route.ts

Local Development Tips

For a detailed walkthrough of local development setup, build, install, and debugging workflows, see Local Development Guide.

Quick reference:

  • Use the pluggable runner backend to switch between Knative (RUNNER_BACKEND=knative) and the local Docker runner (RUNNER_BACKEND=docker).
    • Start the runner container: npm run runner:up or docker compose -f docker-compose.runner-dev.yml up (stops with npm run runner:down).
    • Launch the app with Docker backend defaults: npm run dev:runner (sets RUNNER_DOCKER_HOST=http://localhost:${RUNNER_DOCKER_PORT:-8085} and proxies UI assets through /runner/*).
    • Install your extension locally: node scripts/dev-install-extension.mjs ./path/to/extension.
    • Override runner URLs via .env.runner (see .env.runner.example) if you are using alternative S3 or registry endpoints.
  • Keep UI bundles small; leverage code splitting where appropriate
  • Use the iframe SDK's theme APIs to adapt to host styling
  • Validate endpoint inputs/outputs and include clear error responses

Best Practices

  • Least privilege: request only necessary capabilities
  • Input validation and resource bounds: avoid large responses; paginate/filter server-side
  • Emit structured logs/metrics via Host API to support observability
  • Prefer deterministic builds; maintain SBOMs; update dependencies proactively

References