---
title: Self-Hosted Registry
description: Run your own ggui gadget/blueprint registry with @ggui-ai/registry-server — filesystem-backed, bearer-token auth, byte-compatible with registry.ggui.ai.
---

`@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

| 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`](https://www.npmjs.com/package/@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

```bash
# 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/`.

## 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:

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

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

```bash
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`](/sdk/marketplace/#auth)). Self-hosted registries always use the bearer flag.

## 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

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

```ts
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.

## Building your own transport

If you're not running Hono or Node, target [`@ggui-ai/registry-core`](https://www.npmjs.com/package/@ggui-ai/registry-core) directly. The two seams are storage and authn:

```ts
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:

```ts
import { registryStorageContract, bundleStorageContract } from "@ggui-ai/registry-core/testing";

describe("my custom storage", () => {
  registryStorageContract(() => new MyRegistryStorage());
  bundleStorageContract(() => new MyBundleStorage());
});
```

## 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`, 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.

## Reference

- **Marketplace overview** — [`/sdk/marketplace`](/sdk/marketplace/)
- **Gadgets SDK** — [`/sdk/gadgets`](/sdk/gadgets/)
- **CLI** — [`/cli`](/cli/)
- **Protocol spec** — [Protocol overview](/protocol/overview/)
- **Source** — [`packages/registry-server`](https://github.com/ggui-ai/ggui/tree/main/packages/registry-server) (transport) + [`packages/registry-core`](https://github.com/ggui-ai/ggui/tree/main/packages/registry-core) (shared ops)