Hosting Provider Integration Guide
This guide walks you through integrating @better-openclaw/core into your hosting platform to offer users a marketplace of 180+ self-hosted services. The addon stack API handles Docker Compose generation, secret management, port allocation, and skill configuration — your platform just needs to orchestrate the output.
Architecture Overview
The integration follows a clean separation of concerns:
- Your platform manages infrastructure: servers, PostgreSQL, Redis, reverse proxy, and the OpenClaw gateway.
- @better-openclaw/core generates addon layers: Docker Compose overlays, environment files, proxy routes, and skill definitions.
┌─────────────────────────────────────────────┐
│ Your Hosting Platform │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Dashboard │ │ API/CLI │ │ Provision │ │
│ │ (UI) │→ │ Backend │→ │ Engine │ │
│ └──────────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │
│ ┌──────┴──────┐ │ │
│ │ @better- │ │ │
│ │ openclaw/ │ │ │
│ │ core │ │ │
│ └──────┬──────┘ │ │
│ │ │ │
│ ┌──────┴──────────────┴──┐ │
│ │ Docker Host │ │
│ │ ┌─────┐ ┌──────────┐ │ │
│ │ │Infra│ │ Addons │ │ │
│ │ │Stack│ │ Overlay │ │ │
│ │ └─────┘ └──────────┘ │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────┘Step 1: Install the Core Package
npm install @better-openclaw/coreThe package is a pure TypeScript library with no native dependencies. It works in Node.js, Deno, Bun, and edge runtimes.
Step 2: Generate the Initial Stack
When a user selects services from your marketplace, call generateAddonStack():
import { generateAddonStack } from "@better-openclaw/core";
async function provisionAddons(userId: string, services: string[]) {
// Your infrastructure's occupied ports
const reservedPorts = [80, 443, 3000, 3456, 5432, 6379, 8080];
const result = generateAddonStack({
instanceId: `user-${userId}`,
services,
reservedPorts,
generateSecrets: true,
platform: "linux/amd64",
});
// Handle skipped services (missing credentials, no GPU, etc.)
for (const skipped of result.metadata.skippedServices) {
if (skipped.reason === "missing_credentials") {
// Prompt user for API keys
await requestCredentials(userId, skipped.serviceId, skipped.requiredCredentials);
return;
}
if (skipped.reason === "gpu_required") {
// Show upgrade prompt
await suggestGpuPlan(userId, skipped.serviceId);
}
}
// Write files to the instance directory
await writeFile(`/instances/${userId}/docker-compose.override.yml`, result.composeOverride);
await writeFile(`/instances/${userId}/.env.addons`, result.envFile);
// Write skill files
for (const [path, content] of Object.entries(result.skillFiles)) {
await writeFile(`/instances/${userId}/skills/${path}`, content);
}
// Register proxy routes with your reverse proxy
await registerProxyRoutes(userId, result.proxyRoutes);
// Merge openclaw.json config
await patchOpenclawConfig(userId, result.openclawConfigPatch);
// Start the addon containers
await exec(`docker compose -f docker-compose.override.yml up -d`, {
cwd: `/instances/${userId}`,
});
return {
services: result.metadata.resolvedServices,
ports: result.metadata.portAssignments,
warnings: result.warnings,
};
}Step 3: Handle Service Updates
When users add or remove services from their running instance, use updateAddonStack() instead of regenerating from scratch:
import { updateAddonStack } from "@better-openclaw/core";
async function updateUserAddons(
userId: string,
addServices: string[],
removeServices: string[],
) {
// Read current state
const currentCompose = await readFile(`/instances/${userId}/docker-compose.override.yml`);
const currentEnv = await readFile(`/instances/${userId}/.env.addons`);
const result = updateAddonStack({
instanceId: `user-${userId}`,
currentCompose,
currentEnv,
addServices,
removeServices,
generateSecrets: true,
});
// Pull new images before stopping old services
for (const image of result.imagesToPull) {
await exec(`docker pull ${image}`);
}
// Write updated files (env values for existing services are preserved)
await writeFile(`/instances/${userId}/docker-compose.override.yml`, result.composeOverride);
await writeFile(`/instances/${userId}/.env.addons`, result.envFile);
// Update proxy routes
await registerProxyRoutes(userId, result.proxyRoutes);
// Recreate only changed services
await exec(`docker compose up -d --remove-orphans`, {
cwd: `/instances/${userId}`,
});
return {
added: result.metadata.added,
removed: result.metadata.removed,
unchanged: result.metadata.unchanged,
};
}Step 4: Integrate Proxy Routes
Each addon service that exposes a web UI gets a proxy route. Integrate these with your reverse proxy:
// Caddy integration example
function generateCaddySnippet(routes: ProxyRoute[]): string {
return routes.map(route => {
const upstream = `${route.serviceId}:${route.port}`;
if (route.stripPrefix) {
return `handle_path ${route.path}/* {
reverse_proxy ${upstream}
}`;
}
return `handle ${route.path}/* {
reverse_proxy ${upstream}
}`;
}).join("\n\n");
}
// Traefik integration example
function generateTraefikLabels(route: ProxyRoute): Record<string, string> {
const name = route.serviceId.replace(/-/g, "");
return {
[`traefik.http.routers.${name}.rule`]:
`PathPrefix(\`${route.path}\`)`,
[`traefik.http.services.${name}.loadbalancer.server.port`]:
String(route.port),
};
}Step 5: Present the Service Catalog
Use the service registry to build your marketplace UI:
import {
getAllServices,
getServicesByCategory,
SERVICE_CATEGORIES,
} from "@better-openclaw/core";
// Get all 180+ services with metadata
const services = getAllServices();
// Group by category for UI tabs
for (const cat of SERVICE_CATEGORIES) {
const categoryServices = getServicesByCategory(cat.id);
console.log(`${cat.icon} ${cat.name}: ${categoryServices.length} services`);
}
// Each service definition includes:
// - id, name, description, category
// - image (Docker image), ports, volumes
// - environment variables with descriptions
// - memoryMB (estimated RAM usage)
// - maturity ("stable" | "beta" | "experimental")
// - proxyPath (reverse proxy path)
// - capDropCompatible (security hardening support)Step 6: Offer Skill Packs
Skill packs bundle multiple services into task-focused capabilities:
import {
getAllSkillPacks,
getCompatibleSkillPacks,
} from "@better-openclaw/core";
// Get skill packs compatible with user's current services
const packs = getCompatibleSkillPacks(["n8n", "qdrant"]);
// Each pack includes:
// - id, name, description
// - requiredServices: string[]
// - optionalServices: string[]
// - skills: SkillBinding[] (MCP tool definitions)Security Best Practices
- Always validate input with
AddonStackInputSchema.parse()before passing user data togenerateAddonStack(). - Never expose secrets — the
envFilecontains generated passwords. Store it securely and never return it to the frontend. - Use reserved ports to prevent addon services from claiming ports your infrastructure needs.
- Review skipped services — a
missing_credentialsskip means the service needs user-provided API keys. Don't deploy it without them. - Network isolation — the generated compose uses
openclaw-networkas an external network. Ensure your Docker network configuration restricts inter-service communication appropriately.
Database Integration
Services that need PostgreSQL (like n8n, Grafana, Gitea) automatically get a postgres-setup init container that:
- Connects to your existing PostgreSQL instance
- Creates a dedicated database and user for each service
- Uses idempotent SQL ("IF NOT EXISTS") for safe re-runs
- Generates unique passwords per service
Your PostgreSQL must be reachable at the hostname postgresql on the Docker network. The init container uses the POSTGRES_USER and POSTGRES_PASSWORD from the infrastructure's environment.
Monitoring Integration
Each service definition includes a healthCheck configuration. Use it to set up monitoring:
import { getServiceById } from "@better-openclaw/core";
const n8n = getServiceById("n8n");
console.log(n8n?.healthCheck);
// {
// test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"],
// interval: "30s",
// timeout: "10s",
// retries: 3,
// startPeriod: "30s"
// }See the Addon Stack API Reference for complete parameter and return type documentation.