Install this skill

Paste this into Claude Code (VS Code panel, Adom editor, or terminal) to install:

Search the Adom Wiki for the skill "Standalone Service" (slug: standalone-service) at https://wiki-ufypy5dpx93o.adom.cloud/wiki/skills/standalone-service and install it into my local ~/.claude/skills/standalone-service/ directory. Fetch the skill_source content from the wiki page and save it as SKILL.md. Then confirm it's installed by showing the first 5 lines.
?
What is a skill? Skills are instructions that teach AI assistants like Claude Code how to perform specific tasks. The description below is loaded into the AI as context when you invoke this skill. Well-written skills make the AI significantly more effective. Like Wikipedia, anyone can improve a skill by clicking Edit AI Skill — or have your AI submit an edit on your behalf.

Description

Edit AI Skill

Creating Standalone Adom Services

A standalone service runs on its own dedicated lightweight container, separate from user containers. Use this pattern when:

  • The service requires a large dataset (hundreds of MB+) that shouldn't be duplicated per container
  • The service needs a persistent server process (HTTP API, background jobs, scheduled updates)
  • Multiple containers should share the same data and service instance

Architecture Overview

Each service lives in its own GitHub repo (not in gallia) and runs on a lightweight container (sshd-only, ~7MB RAM idle). Users access it via a Rust CLI that gets installed alongside adom-cli.

Dev Container                 GitHub                     Service Container (default-light)
  service repo dev         --> push --> adom-inc/service-X --> SSH in, git pull
                                                                 |
                                                              node server.js (port XXXX)
                                                                 |
User Containers ------------- Rust CLI (HTTP) ------------------>|

Step 1: Choose Access Pattern

Ask the user which access pattern they want:

Option A: Rust CLI (recommended for new services)

A compiled Rust binary that wraps the service HTTP API. Installed alongside adom-cli on every user container.

User Container                    Service Container
  Claude --> CLI command           HTTP API (port XXXX)
    |                                ^
  service-cli search "query"         |
    |                                |
  HTTP request ------------------->  |
    |
  pushToViewer() --> AV

Best when:

  • The API has multiple endpoints
  • You want zero-overhead on user containers (no background process)
  • You want consistent UX with adom-cli

Option B: Skill-driven HTTP (e.g., KiCad CLI -- services/kicad-cli)

Skills teach Claude the HTTP endpoints directly. Claude uses curl or fetch.

User Container                    Service Container
  Claude --> reads skill           HTTP API (port XXXX)
    |                                ^
  curl via Bash ------- HTTP ------> |

Best when:

  • The API surface is small and stable (handful of endpoints)
  • The service is primarily used by one or two skills
  • You want fewer moving parts (no CLI to build)

Option C: MCP Server (legacy pattern)

MCP stdio process on each user container proxying to the remote API. Not recommended for new services -- use Rust CLI instead. Legacy MCP services (JLCPCB, Mouser, DigiKey, Wiki) still work but won't be the pattern going forward.

Rust CLISkill-driven HTTPMCP Server (legacy)
User container overheadZero (compiled binary)ZeroNode.js stdio process
DiscoveryCLI skill + --helpRequires skill activationAutomatic MCP tools
ValidationCLI argument parsingRelies on skill instructionsJSON Schema
DistributionBinary pulled during installSkill deployed by galliaMCP server.js in gallia
Referenceadom-cliservices/kicad-clijlcpcb/mcp/

Step 2: Create the Service GitHub Repo

Each service gets its own repo at adom-inc/service-<name>. This keeps gallia focused on user-container tooling.

Create the repo on GitHub (or via the Adom API):

# Via GitHub CLI (if authenticated)
gh repo create adom-inc/service-<name> --private --description "Adom <name> service"

# Or via Adom API
API_KEY=$(cat /var/run/adom/api-key)
curl -s -X POST 'https://carbon.adom.inc/user/repos' \
  -b "session_token=$API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"service-<name>","description":"<description>","private":true}'

Step 3: Write the Service Code

Structure the repo like this:

service-<name>/
  server.js          # HTTP API server
  package.json       # Dependencies
  service.json       # Auto-start manifest
  start.sh           # Idempotent start script
  setup.sh           # First-time setup (install deps, init DB, etc.)
  README.md          # Service documentation

service.json -- Service Manifest

This is read by auto-start and watchdog scripts. It's the single source of truth for how to run the service.

{
  "name": "my-service",
  "service": "my-tag",
  "description": "What this service does",
  "port": 8XXX,
  "health": "http",
  "start": "node server.js",
  "cwd": ".",
  "log": "/tmp/my-service.log"
}
FieldRequiredDescription
nameyesService name (used in logs and watchdog output)
serviceyesService filter tag (see Service Filtering below)
descriptionnoHuman-readable description
portyesPort the service listens on
healthyes"http" (GET /health returns 200) or "tcp" (port open check)
startyesShell command to start the service
cwdyesWorking directory -- "." means same dir as service.json
lognoLog file path (defaults to /tmp/<name>.log)

server.js -- HTTP API Server

Use Node.js built-in http module (no Express). Key patterns:

import { createServer } from 'http';

const PORT = parseInt(process.env.MY_SERVICE_PORT || '8XXX', 10);

const server = createServer(async (req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`);

  // Health endpoint (required)
  if (url.pathname === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, service: 'my-service', uptime: process.uptime() }));
    return;
  }

  // Landing page (serve HTML when browser visits)
  if (url.pathname === '/' && req.headers.accept?.includes('text/html')) {
    const proto = req.headers['x-forwarded-proto'] || 'https';
    const host = req.headers['x-forwarded-host'] || req.headers.host;
    const baseUrl = `${proto}://${host}`;
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(landingPageHTML(baseUrl));
    return;
  }

  // Your API endpoints here...
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`[my-service] Running on port ${PORT}`);
});

package.json -- Standalone Dependencies

{
  "name": "service-my-service",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "dependencies": { },
  "scripts": { "start": "node server.js" }
}

start.sh -- Idempotent Start Script

Always the same 3-step pattern:

#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_FILE="/tmp/my-service.log"
PORT=8XXX

# 1. Check if already running (idempotent)
if curl -sf --max-time 2 http://127.0.0.1:${PORT}/health > /dev/null 2>&1; then
  echo "[my-service] Server already running on port ${PORT}"
  exit 0
fi

# 2. Install deps if needed
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
  cd "$SCRIPT_DIR" && npm install --production 2>&1 | tail -3
fi

# 3. Start with nohup
cd "$SCRIPT_DIR"
nohup /usr/bin/node server.js >> "$LOG_FILE" 2>&1 &
echo "[my-service] Server started (PID $!), logging to $LOG_FILE"

Important: Use /usr/bin/node (explicit path) -- PATH may not be set at boot time.

setup.sh -- First-Time Setup

Run once after cloning the repo on the service container:

#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

echo "[setup] Installing dependencies..."
cd "$SCRIPT_DIR" && npm install --production

# Database initialization (if needed)
# node setup-db.js

# Set GALLIA_SERVICE so auto-start knows what to run
echo "GALLIA_SERVICE=my-tag" | sudo tee -a /etc/environment
export GALLIA_SERVICE=my-tag

echo "[setup] Starting service..."
bash "$SCRIPT_DIR/start.sh"

echo "[setup] Verifying health..."
sleep 2
curl -sf http://127.0.0.1:${PORT}/health && echo " OK" || echo " FAILED"

Step 4: Create a Lightweight Container

Use adom-cli to create a default-light container (sshd-only).

Important: Containers must be associated with a repo at creation time -- this cannot be changed later. Always create the repo first (Step 2), get its ID, then pass --repo-id.

# Get the Adom repo ID (from Step 2, or look it up)
adom-cli carbon user repos  # find the "id" field for your service repo

# Create container with repo association
adom-cli carbon containers create \
  --image-id 69b43b3e58d13e5ce628cdc5 \
  --class small \
  --ssh \
  --repo-id <ADOM_REPO_ID>

Note: --repo-id takes the Adom repo ID (from carbon.adom.inc), not a GitHub repo ID.

When associated with a repo, the SSH username includes the repo name (e.g. john-service-wiki-rk6euj7525tq), making it easy to identify. Without --repo-id, the container is orphaned with a random slug and won't appear in repo container listings.

Note the SSH credentials from the response:

{
  "ssh_credentials": {
    "command": "ssh [email protected]",
    "hostname": "adom.cloud",
    "port": 22,
    "username": "john-service-wiki-rk6euj7525tq"
  }
}

Prerequisites: You need an SSH key registered with your Adom account. If you don't have one:

# Generate key if missing
[ ! -f ~/.ssh/id_ed25519 ] && ssh-keygen -t ed25519 -C "adom" -f ~/.ssh/id_ed25519 -N ""

# Register if none on Adom
[ "$(adom-cli carbon user ssh-keys)" = "[]" ] && \
  adom-cli carbon user ssh-key-add --display-name "auto" "$(cat ~/.ssh/id_ed25519.pub)"

Step 5: Deploy the Service

SSH into the container, clone the repo, and run setup:

# SSH in
ssh -o StrictHostKeyChecking=accept-new [email protected]

# Clone the service repo (GitHub auth may be needed)
cd ~ && git clone https://github.com/adom-inc/service-<name>.git service

# Run first-time setup
cd service && bash setup.sh

The default-light image includes node, git, python3, and curl. If your service needs additional tools, install them in setup.sh.

Add a cron @reboot for auto-start

(crontab -l 2>/dev/null; echo "@reboot cd /home/adom/service && bash start.sh >> /tmp/my-service-boot.log 2>&1") | crontab -

Expose a public URL (optional)

If the service needs to be reachable from other containers:

# From your dev container (not from inside the service container)
adom-cli carbon containers port-add <slug> --port 8XXX --prefix service-name

This creates a public URL like service-name-abc123.adom.cloud.

Step 6: Wire Up Consumer Access

Back on the dev container, make the service accessible to all users.

For Rust CLI services (Option A)

  1. Create a Rust CLI project that wraps the HTTP API (follow adom-cli patterns)
  2. Add the CLI binary to gallia's install.mjs so it gets pulled during installation
  3. Create a skill at gallia/skills/<service-name>/SKILL.md documenting the CLI commands

For skill-driven services (Option B)

Document the HTTP endpoints in a gallia skill. Include the service's public URL:

## API Endpoints

Base URL: `https://service-name-abc123.adom.cloud`

### Search
GET /search?q=<query>&limit=10

Hardcode the service URL

Whether in a CLI or skill, hardcode the public URL as the default:

https://<service-url>.adom.cloud/

Create a port mapping with adom-cli carbon containers port-add. For example:

https://service-name-abc123.adom.cloud

Service Filtering (GALLIA_SERVICE)

The GALLIA_SERVICE environment variable (set in /etc/environment) tells auto-start scripts which services to manage on this container.

How filtering works

GALLIA_SERVICE valueWhat starts
(unset or empty)Defaults to "local" -- only services tagged "local"
"wiki"Only services tagged "wiki"
"local,wiki"Services tagged "local" OR "wiki"
"all"Every service regardless of tag

For service containers: Set GALLIA_SERVICE=<tag> in /etc/environment so only that service starts on boot.

For user containers: Leave unset. Defaults to "local".

Setting the env var

echo 'GALLIA_SERVICE=my-tag' | sudo tee -a /etc/environment

Deploying Updates

# From your dev container, SSH into the service container and update
ssh [email protected] "cd ~/service && git pull origin main && npm install && pkill -f 'node.*server.js'; bash start.sh"

Or run each step interactively:

ssh [email protected]
cd ~/service
git pull origin main
npm install
pkill -f 'node.*server.js' || true
bash start.sh

Scheduled Maintenance (Cron)

For daily database updates or cleanup:

#!/bin/bash
# /etc/cron.daily/update-my-service-db
CURL_ARGS=(-sfL -o "$STAGING_PATH" --etag-save "$ETAG_FILE")
[ -f "$ETAG_FILE" ] && CURL_ARGS+=(--etag-compare "$ETAG_FILE")
HTTP_CODE=$(curl -w '%{http_code}' "${CURL_ARGS[@]}" "$DB_URL" 2>/dev/null || true)
if [ "$HTTP_CODE" = "304" ]; then exit 0; fi
mv "$STAGING_PATH" "$DB_PATH"
pkill -HUP -f "node server.js"  # zero-downtime reload

Adom Viewer Integration

If your service returns results that benefit from rich display, register it in the AV Service Dashboard.

Edit /home/adom/gallia/viewer/viewer/index.html:

  1. Add an entry to the SERVICE_REGISTRY JS object with: name, icon, desc, container, port, repo, repoPath, mcpName, and optionally local: true
  2. Add an <option value="svc-<key>"> to the <optgroup> "Standalone Services"
  3. Add a skill-card <div> to the About page

The health check, URL generation, and dashboard rendering are all automatic from the registry entry.

Testing

Create test.js in the service repo:

  • Hit local HTTP API directly (use 127.0.0.1, not localhost)
  • Push styled HTML results to Adom Viewer
  • Assert count > 0 before filter assertions -- .every() on empty array returns true
  • Test with real user queries, not just exact IDs

Reference Implementations

ServiceRepoPatternContainer ImageNotes
JLCPCBgallia (legacy)MCP (legacy)default-vscodeSQLite DB, daily cron
Mousergallia (legacy)MCP (legacy)default-vscodeHTTP proxy to Mouser API
DigiKeygallia (legacy)MCP (legacy)default-vscodeOAuth client_credentials
KiCad CLIgallia (legacy)Skill-driven HTTPdefault-vscodeHeavy install (~600MB KiCad 9)
Wikigallia (legacy)MCP (legacy)default-vscodeSQLite + FTS5, asset uploads
New servicesadom-inc/service-XRust CLIdefault-lightRecommended pattern

Troubleshooting

SymptomCauseFix
SSH "Permission denied"No SSH key registeredGenerate and register: ssh-keygen && adom-cli carbon user ssh-key-add
Service starts but health check failsWrong portMatch port in service.json to what server.js listens on
Service doesn't survive rebootNo auto-startAdd @reboot cron entry (see Step 5)
npm install failsMissing system depsInstall via sudo apt-get install <pkg> in setup.sh
Can't access from user containerNo port mapping or wrong URLAdd port mapping: adom-cli carbon containers port-add
Container not found after creationProvisioning delayWait 15-30 seconds, then retry SSH

Sub-Skills
?
What are Sub-Skills?

Sub-skills are community-contributed AI skill extensions for this component. They teach AI assistants about specific tools, configurators, or workflows.

Examples:

  • A manufacturer’s configuration tool for a motor controller
  • A community-written design guide for an amplifier circuit
  • An automated test/validation script for a sensor module

How to add one: Click Add Sub-Skill, provide the URL to your skill and a brief description. Submissions are reviewed by the Adom team before going live.

No sub-skills yet. Be the first to contribute one!

0 revisions · Updated 2026-04-16 10:56:13