Self-Hosted Registry
read as.md @ggui-ai/registry-server is an OSS registry you self-host. It’s byte-compatible with the hosted cloud: gadgets and blueprints published to your registry install the same way they do from registry.ggui.ai. Same wire format, same signature verification, same SRI integrity, same CLI verbs (ggui gadget / ggui blueprint).
Why self-host
Section titled “Why self-host”| Use case | Why self-host |
|---|---|
| Enterprise private gadgets | Air-gapped network; corporate signing keys; data residency mandates |
| Local development | No network round-trip; no auth setup; full lifecycle in CI |
| Integration testing | Predictable per-test registry state; teardown via rm -rf <storage-dir> |
| Custom transport | Implement RegistryStorage + BundleStorage against any database / blob store |
The OSS server shares all business logic with the cloud Lambda handlers via @ggui-ai/registry-core. The transport layer is the only delta — Hono routes for OSS, API Gateway + Lambda for cloud — and every conformance gate, signature check, and row schema is identical.
Quick start
Section titled “Quick start”# 1. Pick a storage root + an auth token (rotate-able)export GGUI_REGISTRY_TOKEN=$(openssl rand -hex 32)mkdir -p ./registry-data
# 2. Run (npx fetches @ggui-ai/registry-server on first use)npx @ggui-ai/registry-server \ --storage=fs:./registry-data \ --port=9001 \ --bundle-host=http://localhost:9001 \ --registry-hostname=localhost:9001Flag syntax. Each flag takes its value via
=(e.g.--storage=fs:./registry-data). Space-separated forms (--storage fs:./registry-data) are rejected by the parser.
The server logs one line: registry-server listening on http://0.0.0.0:9001. Bundles, signatures, and manifests live under ./registry-data/bundles/; row data lives under ./registry-data/state/.
Publishing to a self-hosted registry
Section titled “Publishing to a self-hosted registry”For publish/search the CLI resolves the target registry via three precedence layers: --registry <url> flag, then the GGUI_REGISTRY env var, then ggui.json#registry (walking up from CWD). Install checks ggui.json#registry before the env var and falls back to https://registry.ggui.ai — set GGUI_REGISTRY or pass --registry to keep installs on your self-hosted instance. Publish with the bearer flag:
ggui gadget publish \ --registry http://localhost:9001 \ --auth bearer \ --token "$GGUI_REGISTRY_TOKEN"For day-to-day usage, set the env once:
export GGUI_REGISTRY=http://localhost:9001export GGUI_REGISTRY_TOKEN=<value>
ggui gadget publish --auth bearer # token picked up from envggui blueprint install @my-org/login-form@0.1.0 # installs are unauthenticated todayThe bearer flag is opt-in. Without it, the CLI sends the access token from your stored ggui login session — the default for registry.ggui.ai, which verifies it at the gateway (see Marketplace § Auth). Self-hosted registries always use the bearer flag.
Storage modes
Section titled “Storage modes”| Mode | What it is | When to use |
|---|---|---|
--storage=memory | In-process Maps; lost on restart | Local dev; CI; per-test isolation |
--storage=fs:<path> | JSON rows + filesystem blobs under <path> | Single-node persistent deployments |
| Custom (third-party) | Implement RegistryStorage + BundleStorage directly | Multi-node, S3, DDB, Postgres, anything |
The filesystem mode is the recommended single-node default. It uses exclusive-create (fs.writeFile with { flag: 'wx' }) to back the atomic putArtifactVersionIfAbsent primitive — the load-bearing concurrency guarantee that re-publishes return 409 version_exists deterministically, even under concurrent writers.
The in-memory mode is wired via inMemoryRegistryStorage() + inMemoryBundleStorage({ bundleHost }) from @ggui-ai/registry-core; filesystem mode via createFilesystemRegistryStorage({ root }) + createFilesystemBundleStorage({ root, bundleHost }) from @ggui-ai/registry-server. Both pairs implement the same RegistryStorage + BundleStorage contracts that custom backends must satisfy.
Opt-in publish-time runtime probe
Section titled “Opt-in publish-time runtime probe”createRegistryServer and createRegistryApp accept an optional blueprintProbe: BlueprintProbeRunner. When wired, every blueprint publish runs the static conformance gates and compiles + renders the blueprint’s default export against the manifest’s fixtureProps in a sandboxed Node vm + react-dom server-renderer. Failures return 400 with code: 'blueprint_runtime_probe_failed'. The reference implementation is @ggui-ai/blueprint-probe’s blueprintProbeRunner export.
import { createRegistryServer } from "@ggui-ai/registry-server";import { blueprintProbeRunner } from "@ggui-ai/blueprint-probe";
createRegistryServer({ // …storage, bundleStorage, authn, registryHostname, bundleHost as before blueprintProbe: blueprintProbeRunner,});The registry-server CLI does NOT wire a probe by default — leaving it unset means only the static gates from checkConformance run. Opt in deliberately:
vm.runInContextis not a security boundary. An internal security audit confirmed sandbox escape viarequire('react').useState.constructor. Wire the probe only where the trust boundary is the caller’s own process — CI checks, e2e fixtures, or a registry that publishes are gated behind authenticated, trusted operators. Do not enable it on unauthenticated public publish endpoints.- The probe also pulls in
happy-dom+react-dom/server— non-trivial deps the lean default server intentionally avoids.
If you need the probe in production, embed createRegistryApp programmatically (so you control the deps + can layer additional pre-checks), rather than running registry-server directly.
Building your own transport
Section titled “Building your own transport”If you’re not running Hono or Node, target @ggui-ai/registry-core directly. The two seams are storage and authn:
import { publishArtifact, readArtifact, searchArtifacts, checkConformance, type RegistryStorage, type BundleStorage, type AuthnContext,} from "@ggui-ai/registry-core";
// 1. Implement the two storage interfaces against your backendclass MyRegistryStorage implements RegistryStorage { /* DDB / Postgres / etc. */}class MyBundleStorage implements BundleStorage { /* S3 / GCS / Azure Blob */}
// 2. Verify the caller's credentials in your transport, then build an AuthnContextfunction verifyCaller(req): AuthnContext | undefined { /* JWT / mTLS / signed header */}
// 3. Plug in to your HTTP frameworkapp.post("/publish", async (req, res) => { const authn = verifyCaller(req); if (!authn) return res.status(401).send({ error: "unauthorized", message: "no creds" });
const result = await publishArtifact(req.body, { storage: myStorage, bundleStorage: myBundleStorage, authn, clock: () => new Date(), registryHostname: req.headers.host, });
res.status(result.status).send(result.body);});The registryStorageContract + bundleStorageContract test helpers under @ggui-ai/registry-core/testing let you verify drift-free parity with the reference impls:
import { registryStorageContract, bundleStorageContract } from "@ggui-ai/registry-core/testing";
describe("my custom storage", () => { registryStorageContract(() => new MyRegistryStorage()); bundleStorageContract(() => new MyBundleStorage());});Non-goals
Section titled “Non-goals”The OSS server is intentionally minimal. NOT in scope:
- TLS termination. Run behind nginx / Caddy / a load balancer. The server speaks plain HTTP — operators decide their TLS posture.
- Rate limiting. Use a reverse proxy (nginx
limit_req, Caddyrate_limit) or a CDN. - Backup automation.
cp -r ./registry-data /backup/is the model. The state is plain JSON + blobs. - Multi-tenant org enforcement.
visibility: "private"is a per-row flag; the OSS server treats it as a label that requires any valid bearer token. Real org-scoped access control is a deployment-layer concern. - Public web UI. The OSS server exposes only the HTTP API. The hosted cloud’s browser UI lives in a separate app (
apps/registry-web) that talks to the same/search+/pkg/*routes — point it at your self-hosted base URL if you want the same browsing experience. - Cross-registry mirror. Single-registry only. The hosted cloud and a self-hosted registry are independent stores.
Reference
Section titled “Reference”- Marketplace overview —
/sdk/marketplace - Gadgets SDK —
/sdk/gadgets - CLI —
/cli - Protocol spec — Protocol overview
- Source —
packages/registry-server(transport) +packages/registry-core(shared ops)