Self-hosting

Three depths of self-hosting, depending on how much of SkillOx you want in-house. The scanner engine is Apache-2.0, every dependency is OSS-friendly, and the full stack runs on a single Hetzner CX22 box.

Three depths

  1. CLI-only — install one binary, scan local SKILL.md files, never talk to skillox.io. The simplest deployment.
  2. Scanner-as-library — embed @skillox/scanner in your own Node app. Same engine the hosted service runs; no telemetry.
  3. Full stack — run the web app + API + worker + Postgres + Redis on your own infra. Sign in with your own GitHub OAuth app, point the CLI at your endpoint, mirror the public catalog or skip the crawler entirely.

1. CLI-only

npm i -g skillox

# Scan a single SKILL.md
skillox audit ./skills/my-skill/SKILL.md

# Scan everything under a directory
skillox audit ./skills/

# Lint frontmatter only (no network calls)
skillox lint ./skills/my-skill/SKILL.md

The CLI runs the scanner locally. The only network traffic is the optional skillox install command (which fetches the skill from its source URL — coming soon). For air-gapped environments:

# In an environment with no outbound network:
SKILLOX_OFFLINE=1 skillox audit ./skills/

The full ruleset + grading thresholds are documented at /docs/rules + /docs/grading. Both run in the local binary exactly as they run in the hosted scanner.

2. Scanner-as-library

Embed the scanner in your own Node app. Useful when you have a custom skill format wrapper or you want the audit to run alongside other analysis in the same process.

import { scan } from '@skillox/scanner';

const content = await Deno.readTextFile('./SKILL.md');
const result = await scan({ content, repoMeta: null });

console.log(result.grade, result.score);
for (const f of result.findings) {
  console.log(`  [${f.severity}] ${f.title} (line ${f.line})`);
}

The @skillox/scanner package has zero runtime deps beyond a markdown parser and is published to your private registry as part of the Apache-2.0 release. Pin against a tag rather than main — new rules ship with each minor version.

3. Full stack

Topology

The minimum production topology — five processes, two stateful services, one reverse proxy:

┌──────────────────┐
│ Cloudflare (TLS) │   optional — bring your own edge
└─────────┬────────┘
          │ :443
┌─────────▼────────┐
│ Caddy            │   reverse proxy, ACME, gzip/br
└───┬─────────┬────┘
    │         │
┌───▼───┐ ┌───▼────┐
│ web   │ │ api    │   Next.js 16, Hono
│ :3000 │ │ :3001  │
└───────┘ └────┬───┘
               │
        ┌──────▼───────┐ ┌──────────┐
        │ Postgres 17  │ │ Redis 7  │
        │ + B2 backups │ │ ephemeral│
        └──────────────┘ └──────────┘

Environment

# .env.local — only the keys the stack actually needs
DATABASE_URL=postgresql://skillox:strong-pw@localhost:5432/skillox_prod
REDIS_URL=redis://localhost:6379
NEXT_PUBLIC_SITE_URL=https://skillox.your-domain.com
NEXT_PUBLIC_API_BASE=https://api.skillox.your-domain.com

# Anti-abuse on the public scan endpoint. Use Cloudflare's always-pass dev
# token for staging; real site key for prod.
TURNSTILE_SECRET_KEY=…

# Random 24+ char string. Hashes IPs for rate limiting.
IP_HASH_SALT=…

# Auth.js v5 — required only if you want sign-in / creator portal
AUTH_SECRET=$(openssl rand -base64 32)
GITHUB_OAUTH_CLIENT_ID=…
GITHUB_OAUTH_CLIENT_SECRET=…

# Optional — only set if you want the Pro tier surfaces enabled
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

# Lifts the unauthenticated 60-req/hr GitHub ceiling to 5,000/hr for the
# crawler. Use a no-scope PAT.
GITHUB_TOKEN=

STRIPE_SECRET_KEY is intentionally optional. When unset, every billing route soft-fails with a 503 response and the pricing surface stays hidden — useful for internal deployments where you don't need the Pro tier surfaces.

Deploy

# 1. Provision a server with at least 4 GB RAM (Hetzner CX22 is what we use)
# 2. Clone the source
git clone https://git.skillox.io/skillox/skillox.git
cd skillox/app

# 3. Install pnpm + node 20+
corepack enable
pnpm install

# 4. Create .env.local (copy the example block above)
cp .env.example .env.local
$EDITOR .env.local

# 5. Run the DB migrations
pnpm --filter @skillox/db db:migrate

# 6. Build + start under systemd
pnpm build
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now skillox-web skillox-api skillox-worker

# 7. Point Caddy or another reverse proxy at :3000 (web) + :3001 (api)

Caddy is what we use in prod (one binary, automatic Let's Encrypt, gzip + brotli). The repo includes a working deploy/Caddyfile and systemd units for web + api + worker under deploy/systemd/.

Backups

Postgres is the only thing that needs durable backups. Our default is a nightly pg_dump piped through age (client-side encryption) into a Backblaze B2 EU bucket. Logs of the backup job land in the same systemd journal as the worker so failures show up in your standard log aggregator.

Redis state is recoverable from cold (the queue refills from scans.status = 'pending'); no separate backup needed.

Running your own catalog

The crawler is an opt-in script, not a daemon. Trigger it manually:

pnpm --filter @skillox/api crawl -- --source github --limit 5000
pnpm --filter @skillox/api crawl -- --source clawhub --limit 5000
pnpm --filter @skillox/api crawl -- --source skillssh --limit 5000
pnpm --filter @skillox/api crawl -- --source all     --limit 10000

Or skip the crawler entirely and rely on creator submissions + manual scans through the public endpoint. The catalog table is populated by whichever ingest path you run — both work.

What you give up self-hosting

Support

Community support via the issue tracker at git.skillox.io. Production support contracts available for Team + Enterprise (see /pricing) including custom rule packs + private channel response SLAs.

Running a self-hosted instance in production? Email hello@skillox.io — we keep an informal log of deployments and pass relevant security advisories to operators directly when something needs fixing fast.