cat docs/architecture.md

Architecture

Stateless Rust + Axum server, three served surfaces on one router, all shared state in Postgres and object storage. Plugins are a composition layer above skills/agents/commands — the surfaces Claude Code consumes (marketplace.json + smart-HTTP git) are public read; everything else is tenant-scoped Bearer.

Components

flowchart LR subgraph dev[Developer machine] CLI[skill-pool CLI] CC["Claude Code
/plugin marketplace add
/plugin install"] HOOKS["hooks
Stop · SessionEnd · SessionStart"] end subgraph server[skill-pool server · Rust + Axum] API["REST API
/v1/skills · /v1/plugins"] MARKET["/.claude-plugin/
marketplace.json"] GIT["/git/plugins/<slug>.git
(smart HTTP)"] L1["Layer 1
skills · agents · commands"] L2["Layer 2
projects · plans"] L3["Layer 3
plugins (composition)"] CAP["Capturer daemon
Haiku → Sonnet"] API --> L1 API --> L2 API --> L3 L3 -.composes.-> L1 MARKET -.reads.-> L3 GIT -.reads.-> L3 end Web[skill-pool-web
per-tenant brand] subgraph data[Storage] PG[(Postgres 17 + pgvector)] OBJ[(opendal · fs / S3 / GCS)] REDIS[(Redis · cache + queue, optional)] end CLI -->|HTTPS Bearer| API Web -->|HTTPS session| API CC -->|GET marketplace.json| MARKET CC -->|git clone| GIT HOOKS -->|score · queue| CAP CAP -->|POST /v1/drafts| API API --> PG API --> OBJ API -.-> REDIS L3 --> OBJ

Three served surfaces sit on the same Axum router:

  1. REST /v1/* — tenant-scoped, Bearer-authed. The CLI and the Svelte portal speak to this.
  2. /.claude-plugin/marketplace.json — public read. Claude Code's /plugin marketplace add <host> hits this directly.
  3. /git/plugins/<slug>.git — public read, smart-HTTP. Claude's /plugin install clones it.

Plugins are compositions: a published plugin row pins existing catalogue entries by (slug, kind, version). No duplication.

Process boundaries

Tenancy

ModeWhenLayout
Shared Default · most deployments one server / DB / bucket; every row carries tenant_id; subdomain routing.
Dedicated Regulated tenants (SOC 2, FedRAMP, GxP) one server / DB / bucket per tenant; same image, different DSN.

Data flow — publish

CLI: skill-pool publish ./my-skill/
  → tar+gzip the directory                  (client)
  → POST /v1/skills (multipart)             (HTTPS)
    → tenant + auth extraction              (server)
    → bundle.tar.gz lint + secret scan      (server)
    → SHA-256 + upload to object storage    (server → opendal)
    → INSERT INTO skills (...)              (server → Postgres)
    → INSERT INTO audit_events (...)        (server → Postgres)
  ← 201 Created with canonical metadata     (server → CLI)

Data flow — install

CLI: skill-pool ensure
  → load .skill-pool/manifest.toml
  → for each skill not yet in ~/.skill-pool/library/<tenant>/<slug>@<ver>/:
      GET /v1/skills/{slug}/bundle.tar.gz   (HTTPS; signed S3 URL)
      extract to library
  → symlink library entry into .claude/skills/

Data flow — plugin install (Claude Code)

User: /plugin marketplace add https://skills.acme.com
  → Claude GET https://skills.acme.com/.claude-plugin/marketplace.json
  ← list of plugins available to your tenant
User: /plugin install acme-platform-bundle
  → Claude git clone https://skills.acme.com/git/plugins/acme-platform-bundle.git
  ← plugin manifest + skills/agents/commands/hooks/mcp servers
Public-read by design. Claude Code does not authenticate to these endpoints — tenant routing is by subdomain / custom domain + ACME, and content is what the curator chose to publish. No private data crosses these surfaces.

Data flow — retrospective capture

Claude Code Stop-hook fires after every session.
  → deterministic scorer: token count · time-on-task · edit-density · test-run-ratio
  → if score > threshold: SessionEnd-hook writes transcript to ~/.skill-pool/capture-queue/
capturer daemon (systemd · per-user):
  → claim oldest queue file
  → Haiku extractor: 1-pass summary → topic + tags + intent
  → Sonnet drafter: full transcript → SKILL.md draft + frontmatter
  → POST /v1/drafts (server)
Reviewer opens /portal/drafts:
  → diff against existing skills
  → edit · approve · publish → becomes a new skill version

Key invariants

These are load-bearing. Code review + test suites enforce them.

  1. Every business query filters by tenant_id. A static extractor (server/tests/tenant_filter_static.rs) parses every sqlx::query! in the codebase and fails CI on any without a tenant_id = predicate.
  2. Object storage keys are tenant-prefixed ({tenant_id}/...). Bundle URIs in DB are opaque to the client.
  3. Audit log writes are non-optional. Every mutating endpoint writes an audit_events row in the same tx.
  4. App is stateless. Safe to replicate horizontally; no sticky sessions.

Stack at a glance

LayerTech
ServerRust · Axum · sqlx · tokio · opendal · redis-rs
DatabasePostgres 17 · pgvector 0.7
WebSvelteKit 2 · Svelte 5 (runes) · Tailwind v4
CLIRust · clap · reqwest
Capturedeterministic scorer + Haiku extractor + Sonnet drafter
AuthOIDC (openidconnect) · SAML 2.0 (samael) · SCIM v2
Observabilitytracing-subscriber · Prometheus /metrics · OTLP exporter
InfraNix flake · Helm chart · Terraform AWS · Caddy (ACME)
Want the per-module mental model? Start with docs/architecture.md, then dive into server/src/ — every route module has a doc comment summarizing its tenancy contract.