PSA/ee/docs/extension-system/local-development.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

25 KiB

Local Development Guide: Running Extensions with Docker Runner

This guide explains how to set up and run Alga PSA extensions locally using the Docker-based Runner for iterative development and debugging.

Overview

The local development workflow allows you to:

  • Build and test extension handlers (WASM components) in isolation
  • Run extensions in the Docker-based Runner without full production infrastructure
  • Test UI components in iframes with hot-reload
  • Debug component execution with structured logging and debugging tools
  • Iterate rapidly without pushing to production

Prerequisites

  • Docker and Docker Compose
  • Node.js 18+
  • An Alga PSA extension project or template (see development_guide.md)
  • The main app server running locally (for the gateway)
  • (Optional) The Alga CLI for streamlined builds: npm install -g @alga-psa/cli

Architecture: Local Runner Setup

Local Dev Environment:
┌─────────────────────────────────────────────────────────────────┐
│ Host Machine                                                    │
│                                                                 │
│  ┌──────────────────┐              ┌─────────────────────────┐ │
│  │ Extension Project│              │ Alga App Server         │ │
│  │ (WASM + UI)      │              │ (Next.js)               │ │
│  └────────┬─────────┘              └────────┬────────────────┘ │
│           │                                  │                  │
│           │ $ npm run build                  │ $ npm run dev    │
│           │                                  │                  │
│           │  Generated:                      └───────┬──────────┘
│           │  - dist/main.wasm                        │
│           │  - ui/dist/index.html                    │
│           │  - manifest.json                         │
│           ▼                                          │
│  ┌──────────────────────────┐    API Gateway         │
│  │ Extension Bundle Store   │◄──────────────────────┘
│  │ (temp directory)         │   GET /api/ext/{id}
│  └──────────────────────────┘   POST /api/ext/{id}/...
│           ▲                                                      │
│           │ (fetch bundle)                                       │
│           │                                                      │
│  ┌────────┴─────────────────────────────────────────────────┐   │
│  │ Docker Container: Extension Runner                       │   │
│  │ ┌──────────────────────────────────────────────────────┐ │   │
│  │ │ Service: extension-runner                            │ │   │
│  │ │ Port: 8085 (mapped from 8080)                       │ │   │
│  │ │                                                      │ │   │
│  │ │ - POST /v1/execute: Run handlers                    │ │   │
│  │ │ - GET /ext-ui/{id}/{hash}/: Serve UI assets        │ │   │
│  │ │ - Wasmtime: Execute WASM components                │ │   │
│  │ │ - Static file server for iframe assets             │ │   │
│  │ └──────────────────────────────────────────────────────┘ │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Quick Start

1. Start MinIO (Bundle Storage)

The extension runner fetches bundles from S3-compatible storage. For local development, use MinIO:

# Start MinIO container (port 4569 for API, 4570 for console)
docker run -d --name alga_minio \
  -p 4569:9000 -p 4570:9001 \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

# Create the extensions bucket
docker run --rm --network host --entrypoint /bin/sh minio/mc -c \
  "mc alias set myminio http://localhost:4569 minioadmin minioadmin && mc mb myminio/extensions"

Verify MinIO is running:

2. Start the Docker Runner

From the Alga app root:

docker compose -f docker-compose.runner-dev.yml up --build

This starts the extension-runner container on localhost:8085.

Check it's running:

curl http://localhost:8085/healthz
# or check logs:
docker logs -f alga_extension_runner

3. Start the Main App Server

In another terminal, from the app root:

cd server && PORT=3004 npm run dev

This runs the gateway that proxies /runner/ requests to the extension runner.

Important: Make sure your server/.env has the correct runner configuration:

RUNNER_BACKEND=docker
RUNNER_BASE_URL=http://localhost:8085
RUNNER_DOCKER_HOST=http://localhost:8085
RUNNER_PUBLIC_BASE=/runner
RUNNER_SERVICE_TOKEN=local-runner-key

4. Build Your Extension

From your extension project root:

Using the Alga CLI (recommended):

alga build
# Automatically compiles TypeScript and creates WASM component
# Outputs: dist/main.wasm (for WASM extensions)

Using npm scripts:

npm run build
npm run build:component
# Produces: dist/main.wasm, ui/dist/**, manifest.json

For simple UI-only extensions (like the hello-world sample), you only need the UI files and manifest.

5. Create and Upload the Bundle

The runner expects bundles as .tar.zst archives in MinIO. Here's the complete process:

# 1. Create the bundle archive from your extension directory
cd ./path/to/extension
tar --zstd -cvf /tmp/my-extension-bundle.tar.zst manifest.json ui/
# For extensions with WASM handlers, include dist/:
# tar --zstd -cvf /tmp/my-extension-bundle.tar.zst manifest.json ui/ dist/

# 2. Calculate the SHA256 hash of the bundle
BUNDLE_HASH=$(shasum -a 256 /tmp/my-extension-bundle.tar.zst | cut -d' ' -f1)
echo "Bundle hash: $BUNDLE_HASH"

# 3. Upload to MinIO with the correct path structure
# Path format: tenants/{tenant_id}/extensions/{extension_id}/sha256/{hash}/bundle.tar.zst
TENANT_ID="your-tenant-uuid"  # Get this from the app UI or database
EXT_ID="your-extension-uuid"   # Generated by the install script

docker run --rm --network host --entrypoint /bin/sh -v /tmp:/tmp minio/mc -c \
  "mc alias set myminio http://localhost:4569 minioadmin minioadmin && \
   mc cp /tmp/my-extension-bundle.tar.zst myminio/extensions/tenants/${TENANT_ID}/extensions/${EXT_ID}/sha256/${BUNDLE_HASH}/bundle.tar.zst"

6. Install Extension Metadata in Database

# Set database connection (adjust for your environment)
export PGPASSWORD=$(cat secrets/postgres_password)

# Run the install script
DB_HOST=localhost DB_PORT=5436 node scripts/dev-install-extension.mjs ./path/to/extension

Important: The install script uses a default tenant ID. You may need to update it:

# Find your actual tenant ID (visible in the app header or query the database)
psql -h localhost -p 5436 -U postgres -d server -c "SELECT tenant FROM tenants LIMIT 1;"

# Update the install to use your tenant
psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE tenant_extension_install SET tenant_id = 'your-actual-tenant-id' WHERE registry_id = 'extension-registry-id';"

Update the content hash to match your bundle:

psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE extension_bundle SET content_hash = 'sha256:${BUNDLE_HASH}' WHERE version_id = 'your-version-id';"

7. Test Your Extension

Restart the runner (to clear any cached failures):

docker restart alga_extension_runner

Load the UI in a browser:

http://localhost:3004/msp/extensions/{extension-registry-id}/

The extension should appear in the sidebar menu under "EXTENSIONS" and load in an iframe when clicked.

Call your handler via the gateway (for server-to-server testing):

curl -X POST http://localhost:3004/api/ext/{extension-id}/path \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"data": "test"}'

Note: For UI→Handler communication, use the postMessage proxy pattern instead of direct fetch() calls. See the Development Guide for details.

Complete Example: Hello World Extension

Here's a complete walkthrough using the built-in hello-world sample extension:

# 1. Start MinIO
docker run -d --name alga_minio \
  -p 4569:9000 -p 4570:9001 \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio server /data --console-address ":9001"

docker run --rm --network host --entrypoint /bin/sh minio/mc -c \
  "mc alias set myminio http://localhost:4569 minioadmin minioadmin && mc mb myminio/extensions"

# 2. Start the extension runner
docker compose -f docker-compose.runner-dev.yml up --build -d

# 3. Create the bundle
cd ee/extensions/samples/hello-world
tar --zstd -cvf /tmp/hello-world-bundle.tar.zst manifest.json ui/

# 4. Calculate the hash
BUNDLE_HASH=$(shasum -a 256 /tmp/hello-world-bundle.tar.zst | cut -d' ' -f1)
echo "Bundle hash: $BUNDLE_HASH"

# 5. Install extension metadata (from repo root)
cd ../../../..
export PGPASSWORD=$(cat secrets/postgres_password)
DB_HOST=localhost DB_PORT=5436 node scripts/dev-install-extension.mjs ./ee/extensions/samples/hello-world

# The script outputs the registry_id and version_id - note these for the next steps
# Example output:
#   Registry ID: 3b8b0204-25d9-57d0-951b-3ed518145469
#   Version entry created/updated: d1164241-1ba8-525c-bcee-689e6fa1a534

# 6. Get your tenant ID (check the app header after logging in, or query):
psql -h localhost -p 5436 -U postgres -d server -c "SELECT tenant FROM tenants LIMIT 1;"
# Example: 1573867a-384d-4555-a206-bcfd86440ac1

# 7. Update tenant_extension_install to use your tenant
TENANT_ID="1573867a-384d-4555-a206-bcfd86440ac1"  # Use your actual tenant ID
EXT_ID="3b8b0204-25d9-57d0-951b-3ed518145469"     # Use the registry_id from step 5

psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE tenant_extension_install SET tenant_id = '${TENANT_ID}' WHERE registry_id = '${EXT_ID}';"

# 8. Update the content hash in the database
VERSION_ID="d1164241-1ba8-525c-bcee-689e6fa1a534"  # Use the version_id from step 5

psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE extension_bundle SET content_hash = 'sha256:${BUNDLE_HASH}' WHERE version_id = '${VERSION_ID}';"

# 9. Upload bundle to MinIO
docker run --rm --network host --entrypoint /bin/sh -v /tmp:/tmp minio/mc -c \
  "mc alias set myminio http://localhost:4569 minioadmin minioadmin && \
   mc cp /tmp/hello-world-bundle.tar.zst myminio/extensions/tenants/${TENANT_ID}/extensions/${EXT_ID}/sha256/${BUNDLE_HASH}/bundle.tar.zst"

# 10. Restart runner to clear any cached errors
docker restart alga_extension_runner

# 11. Start the server (in server/ directory)
cd server && PORT=3004 npm run dev

# 12. Open browser to http://localhost:3004 and log in
# The "Hello World" extension should appear in the sidebar under "EXTENSIONS"

Configuration

Environment Variables

Configure the Runner and Gateway via .env.runner:

# Copy the example:
cp .env.runner.example .env.runner

# Edit to match your setup:

Key variables:

Variable Default Purpose
RUNNER_REGISTRY_BASE_URL http://host.docker.internal:3000/api/internal/ext-runner Gateway's internal URL to Runner (used by gateway to call runner)
RUNNER_BUNDLE_STORE_BASE http://host.docker.internal:4569/extensions Base URL where Runner fetches bundles (object storage or temp dir)
RUNNER_ALGA_AUTH_KEY dev-runner-key Service auth key for runner ↔ gateway communication
RUNNER_DOCKER_PORT 8085 Port mapping from container (8080) to host
RUNNER_STATIC_STRICT_VALIDATION false Strict validation of UI asset hashes (disable for dev)

Additional Runner options (advanced):

# Wasmtime component pool size
WASM_POOL_TOTAL_COMPONENTS=256

# Static file cache location in container
EXT_CACHE_ROOT=/app/tmp-ext

# Max file size for UI assets
EXT_STATIC_MAX_FILE_BYTES=10485760

# HTTP egress allowlist (comma-separated)
EXT_EGRESS_ALLOWLIST=httpbin.org,api.example.com

# Gateway timeout for extension execution
EXT_GATEWAY_TIMEOUT_MS=5000

# Debug Redis stream (optional, see debugging section)
RUNNER_DEBUG_REDIS_URL=redis://host.docker.internal:6379
RUNNER_DEBUG_REDIS_STREAM_PREFIX=ext:debug

Docker Compose Overrides

If you need to customize the runner container, edit docker-compose.runner-dev.yml:

services:
  extension-runner:
    environment:
      # Override any environment variable here
      EXT_CACHE_ROOT: /custom/cache/path
    ports:
      # Change the host port
      - "9085:8080"
    volumes:
      # Add additional volumes
      - /path/to/bundles:/app/bundles:ro

Then rebuild:

docker compose -f docker-compose.runner-dev.yml up --build

Workflow: Edit → Build → Test

For Handler Changes (WASM)

  1. Edit src/component/handler.ts
  2. Run build:
    npm run build && npm run build:component
    
  3. Reinstall the extension:
    node scripts/dev-install-extension.mjs .
    
  4. Test:
    curl -X POST http://localhost:3000/api/ext/com.example.my-extension/path
    

For UI Changes (Iframe)

  1. Edit src/ui/src/main.tsx or components
  2. Build:
    npm run build    # builds ui/dist
    npm run build:component  # updates main.wasm if handlers changed
    
  3. Reinstall:
    node scripts/dev-install-extension.mjs .
    
  4. Reload the iframe in browser (Cmd+R or F5)

Hot Reload (Optional)

If you want to avoid reinstalling after every build, you can use a Vite dev server for the UI:

npm run ui:dev   # serves ui from localhost:5173

Then in your test app, point the iframe to http://localhost:5173/index.html instead of the Runner's static path. This is not recommended for production, but useful for rapid UI iteration.

Bundle and Installation

Understanding the Installation Script

The dev-install-extension.mjs script:

  1. Reads manifest.json from your extension root
  2. Calculates content hash (SHA256) of the built artifacts:
    • manifest.json
    • dist/main.wasm
    • ui/dist/**/*
  3. Checks for unsigned bundles (dev-only; production requires signatures)
  4. Inserts into database:
    • extension_registry (extension metadata)
    • extension_version (version record)
    • extension_bundle (bundle metadata + content hash)
    • tenant_extension_install (install for local tenant)
  5. Uses deterministic UUIDs based on extension name (for consistency across rebuilds)

Uninstalling an Extension

To remove a locally-installed extension:

node scripts/dev-uninstall-extension.mjs com.example.my-extension

This removes all database records. The next /api/ext/... call will fail with "extension not found".

Debugging

Logs

Runner logs:

docker logs -f alga_extension_runner

Look for:

  • Starting runner on port 8080
  • [POST /v1/execute] handler invocations
  • ERROR or WARN messages

Gateway logs:

# In your app dev terminal
npm run dev    # shows Next.js logs

Look for:

  • [GET /api/ext/...] requests
  • Calling runner at ...
  • ERROR responses

Structured Logging from Handlers

Use @alga-psa/extension-runtime logging to emit structured logs:

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

export const handler: Handler = async (req, host) => {
  // Emit a log message
  await host.logging.emit({
    level: 'info',
    message: 'Processing request',
    fields: { path: req.http.path, method: req.http.method },
  });

  try {
    const data = await host.http.fetch({ url: 'https://api.example.com/data' });
    await host.logging.emit({
      level: 'debug',
      message: 'Upstream response',
      fields: { status: data.status },
    });
    return jsonResponse({ ok: true });
  } catch (err) {
    await host.logging.emit({
      level: 'error',
      message: 'Request failed',
      fields: { error: String(err) },
    });
    return jsonResponse({ ok: false, error: String(err) }, { status: 500 });
  }
};

These logs appear in the Runner logs and can be collected by your observability system.

Debug Stream (Advanced)

For detailed execution traces, set up a Redis debug stream:

  1. Start Redis:

    docker run -p 6379:6379 redis
    
  2. Configure Runner:

    # In .env.runner:
    RUNNER_DEBUG_REDIS_URL=redis://host.docker.internal:6379
    RUNNER_DEBUG_REDIS_STREAM_PREFIX=ext:debug
    
  3. Tail the stream:

    redis-cli
    > XREAD COUNT 10 STREAMS ext:debug 0
    

Each extension execution emits debug events (context, handler start, host API calls, response).

Common Issues

Runner Container Won't Start

Error: Container exited with code 1

Check:

  1. Docker is running: docker ps
  2. Port 8085 is available: lsof -i :8085
  3. Build succeeded: docker compose -f docker-compose.runner-dev.yml build --no-cache
  4. Logs: docker logs alga_extension_runner

Fix: Stop conflicting containers and rebuild:

docker compose -f docker-compose.runner-dev.yml down
docker compose -f docker-compose.runner-dev.yml up --build

Extension Installation Fails

Error: Extension not found in database when calling gateway

Check:

  1. Did the install script complete? node scripts/dev-install-extension.mjs . should show
  2. Is the manifest.json valid? cat manifest.json | jq
  3. Are the build artifacts present?
    ls dist/main.wasm
    ls ui/dist/index.html
    

Fix: Rebuild and reinstall:

npm run build && npm run build:component
node scripts/dev-uninstall-extension.mjs com.example.my-extension || true
node scripts/dev-install-extension.mjs .

404 on Extension Handler Call

Error: POST /api/ext/com.example.my-extension/path returns 404

Check:

  1. Is the gateway running? curl http://localhost:3000/api/health
  2. Is the runner running? curl http://localhost:8085/health
  3. Is the extension installed? Check the database or call with a nonexistent ID (should get different error)

Fix: Restart the gateway:

# Ctrl+C in the dev terminal
npm run dev:runner

WASM Component Panics

Error: ERROR: Execution failed: Wasm trap in runner logs

Check:

  1. Are you using @alga-psa/extension-runtime correctly? Review the development_guide.md.
  2. Is the handler function exported as default? export const handler: Handler = ...
  3. Are you accessing capabilities that aren't granted? Check your manifest capabilities.

Fix: Review the stack trace in logs and the handler implementation.

UI Not Loading in Iframe

Error: Iframe blank or 404 on GET /runner/ext-ui/...

Check:

  1. Is ui/dist/index.html present? ls ui/dist/index.html
  2. Is the content hash correct? (Should match install records in DB)
  3. Are the assets within the bundle? tar -tzf bundle.tar.zst | head

Fix: Rebuild and reinstall:

npm run build
node scripts/dev-install-extension.mjs .
# Hard refresh iframe in browser (Cmd+Shift+R)

Extension Shows {"code":"extract_failed"}

Error: The iframe shows {"code":"extract_failed"} JSON

Cause: The runner cannot fetch or extract the bundle from MinIO.

Check:

  1. Is MinIO running? curl http://localhost:4569/minio/health/live
  2. Does the extensions bucket exist?
  3. Is the bundle uploaded to the correct path?
  4. Check runner logs: docker logs alga_extension_runner

Common issues in runner logs:

  • error sending request for url: MinIO not reachable from the runner container
  • 400 Bad Request: Bundle not found at the expected path
  • HASH_MISMATCH: The content hash in the database doesn't match the actual bundle

Fix:

# Verify bundle exists in MinIO
docker run --rm --network host --entrypoint /bin/sh minio/mc -c \
  "mc alias set myminio http://localhost:4569 minioadmin minioadmin && mc ls myminio/extensions/tenants/ --recursive"

# Re-upload if needed (see step 5 in Quick Start)

Extension Shows {"code":"archive_hash_mismatch"}

Error: The runner fetched the bundle but the hash doesn't match

Cause: The content_hash in the database doesn't match the SHA256 of the uploaded bundle.

Fix:

# 1. Calculate the actual hash of your bundle
shasum -a 256 /tmp/my-extension-bundle.tar.zst

# 2. Update the database with the correct hash
export PGPASSWORD=$(cat secrets/postgres_password)
psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE extension_bundle SET content_hash = 'sha256:YOUR_ACTUAL_HASH' WHERE version_id = 'your-version-id';"

# 3. Re-upload to MinIO with the correct path (using the actual hash)
# 4. Restart the runner to clear cache
docker restart alga_extension_runner

Extension Not Showing in Sidebar Menu

Error: Extension installed but doesn't appear in the sidebar

Cause: The extension is installed for a different tenant than you're logged in as.

Check:

# Find your current tenant (shown in the app header)
# Then check what tenant the extension is installed for:
psql -h localhost -p 5436 -U postgres -d server -c \
  "SELECT tenant_id FROM tenant_extension_install WHERE registry_id = 'your-extension-id';"

Fix:

psql -h localhost -p 5436 -U postgres -d server -c \
  "UPDATE tenant_extension_install SET tenant_id = 'your-actual-tenant-id' WHERE registry_id = 'your-extension-id';"

Then refresh the page.

Advanced: Running Multiple Extensions

You can develop and test multiple extensions simultaneously:

# Terminal 1: Start runner once
docker compose -f docker-compose.runner-dev.yml up

# Terminal 2: Start main app
npm run dev:runner

# Terminal 3+: Install and develop each extension
cd extension-1
npm run build && npm run build:component
node ../../scripts/dev-install-extension.mjs .

cd ../extension-2
npm run build && npm run build:component
node ../../scripts/dev-install-extension.mjs .

Then call each via /api/ext/{extensionId}/... in your tests.

Advanced: Custom Runner Configuration

Building a Modified Runner Locally

If you need to modify the Runner itself (in ee/runner/):

  1. Edit Rust code in ee/runner/src/
  2. Rebuild:
    docker compose -f docker-compose.runner-dev.yml up --build --force-recreate
    
  3. Docker will recompile the Rust binary and restart the container

Bundle Storage Path Structure

The runner expects bundles in MinIO at a specific path structure:

extensions/tenants/{tenant_id}/extensions/{extension_id}/sha256/{content_hash}/bundle.tar.zst

Where:

  • tenant_id: UUID of the tenant (found in app header or tenants table)
  • extension_id: UUID from extension_registry.id
  • content_hash: SHA256 hash of the bundle archive (without sha256: prefix)

Example path:

extensions/tenants/1573867a-384d-4555-a206-bcfd86440ac1/extensions/3b8b0204-25d9-57d0-951b-3ed518145469/sha256/c4bf05d95138f23599c175b28ad6460cd268ad0e498f581526af9e8097318201/bundle.tar.zst

MinIO Configuration in .env.runner

The default .env.runner is pre-configured for MinIO on port 4569:

RUNNER_BUNDLE_STORE_BASE=http://host.docker.internal:4569/extensions
BUNDLE_STORE_BASE=http://host.docker.internal:4569/extensions
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_REGION=us-east-1

If you need to use different ports or credentials, update these values accordingly.

Next Steps

Once you're comfortable with local development:

  1. Write tests — Use @alga-psa/extension-runtime testing utilities
  2. Deploy to staging — Publish your bundle and install on a staging Alga instance
  3. Set up CI/CD — Automate bundling, signing, and publishing (see enterprise-build-workflow.md)
  4. Monitor in production — Use structured logging and debug streams for observability