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

745 lines
25 KiB
Markdown

# 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](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:
```bash
# 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:**
- API: http://localhost:4569
- Console: http://localhost:4570 (login: minioadmin/minioadmin)
### 2. Start the Docker Runner
From the Alga app root:
```bash
docker compose -f docker-compose.runner-dev.yml up --build
```
This starts the `extension-runner` container on `localhost:8085`.
**Check it's running:**
```bash
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:
```bash
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:
```bash
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):**
```bash
alga build
# Automatically compiles TypeScript and creates WASM component
# Outputs: dist/main.wasm (for WASM extensions)
```
**Using npm scripts:**
```bash
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:
```bash
# 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
```bash
# 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:
```bash
# 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:
```bash
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):
```bash
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):**
```bash
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](development_guide.md#calling-your-wasm-handler-from-the-ui-postmessage-proxy-pattern) for details.
## Complete Example: Hello World Extension
Here's a complete walkthrough using the built-in hello-world sample extension:
```bash
# 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`:
```bash
# 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):**
```bash
# 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`:
```yaml
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:
```bash
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:
```bash
npm run build && npm run build:component
```
3. Reinstall the extension:
```bash
node scripts/dev-install-extension.mjs .
```
4. Test:
```bash
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:
```bash
npm run build # builds ui/dist
npm run build:component # updates main.wasm if handlers changed
```
3. Reinstall:
```bash
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:
```bash
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](../../../scripts/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:
```bash
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:**
```bash
docker logs -f alga_extension_runner
```
Look for:
- `Starting runner on port 8080`
- `[POST /v1/execute]` handler invocations
- `ERROR` or `WARN` messages
**Gateway logs:**
```bash
# 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:
```ts
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:**
```bash
docker run -p 6379:6379 redis
```
2. **Configure Runner:**
```bash
# In .env.runner:
RUNNER_DEBUG_REDIS_URL=redis://host.docker.internal:6379
RUNNER_DEBUG_REDIS_STREAM_PREFIX=ext:debug
```
3. **Tail the stream:**
```bash
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:
```bash
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?
```bash
ls dist/main.wasm
ls ui/dist/index.html
```
**Fix:** Rebuild and reinstall:
```bash
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:
```bash
# 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](development_guide.md#building-server-handlers-componentized-wasm).
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:
```bash
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:**
```bash
# 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:**
```bash
# 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:**
```bash
# 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:**
```bash
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:
```bash
# 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:
```bash
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:
```bash
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.
## Related Documentation
- [Development Guide](development_guide.md) — Building and structuring extensions
- [Manifest Schema](manifest_schema.md) — Extension configuration reference
- [Runner](runner.md) — Runner architecture and interfaces
- [Security & Signing](security_signing.md) — Production signing and verification
- [API Routing Guide](api-routing-guide.md) — Extension HTTP endpoints
## 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](enterprise-build-workflow.md))
4. **Monitor in production** — Use structured logging and debug streams for observability