Skip to content

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).

Use caseWhy self-host
Enterprise private gadgetsAir-gapped network; corporate signing keys; data residency mandates
Local developmentNo network round-trip; no auth setup; full lifecycle in CI
Integration testingPredictable per-test registry state; teardown via rm -rf <storage-dir>
Custom transportImplement 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.

Terminal window
# 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:9001

Flag 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/.

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:

Terminal window
ggui gadget publish \
--registry http://localhost:9001 \
--auth bearer \
--token "$GGUI_REGISTRY_TOKEN"

For day-to-day usage, set the env once:

Terminal window
export GGUI_REGISTRY=http://localhost:9001
export GGUI_REGISTRY_TOKEN=<value>
ggui gadget publish --auth bearer # token picked up from env
ggui blueprint install @my-org/login-form@0.1.0 # installs are unauthenticated today

The 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.

ModeWhat it isWhen to use
--storage=memoryIn-process Maps; lost on restartLocal dev; CI; per-test isolation
--storage=fs:<path>JSON rows + filesystem blobs under <path>Single-node persistent deployments
Custom (third-party)Implement RegistryStorage + BundleStorage directlyMulti-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.

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.runInContext is not a security boundary. An internal security audit confirmed sandbox escape via require('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.

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 backend
class 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 AuthnContext
function verifyCaller(req): AuthnContext | undefined {
/* JWT / mTLS / signed header */
}
// 3. Plug in to your HTTP framework
app.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());
});

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, Caddy rate_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.