Registry

Registry Model (Registry-as-a-Service)

The architectural model behind treating a Handoff registry as a service, not a per-client build.

Registry-as-a-service (handoff-app's ADR-001) is the decision that a Handoff registry is one deployment of a generic app, not a per-client build. Concretely:

  1. handoff-app is deployed once per registry, unmodified. It's npm install && npm run build:registry from the handoff-app repository root, no per-client customization, no materialization step. See Deploy.
  2. All per-project content arrives over HTTP, after deploy. Components, pages, tokens, theme CSS, navigation, icons, logos, fonts, project metadata, everything that used to be "baked into the build", is pushed by a workspace and stored as data.
  3. A workspace never deploys itself. It's a local git repo (components, pages, theme.css, handoff.config.*, a design-system/ token pipeline) driven by the handoff-app CLI. It pushes to a registry over HTTP; it never runs its own production deployment.
  4. Single registry, single project, for now. One registry deployment serves one workspace (one figma_project_id). Multi-tenancy (an org_id column, project scoping) is deferred to a later phase, added when client count justifies it, the schema was designed to make that addition purely additive.

The DataProvider pattern

This is the mechanism that makes one codebase serve both roles. Every page component, REST route, and MCP tool reads data through a single interface rather than the filesystem or the database directly:

getDataProvider()
  → DATABASE_URL set     → DynamicDataProvider   (registry: reads/writes Postgres)
  → DATABASE_URL absent  → StaticDataProvider     (workspace dev: reads the local filesystem)

StaticDataProvider reads component artifacts, compiled tokens, markdown pages, and asset catalogs off disk under HANDOFF_WORKING_PATH. DynamicDataProvider reads the equivalent data from Postgres, mostly the singleton config rows and per-entity tables described in Concepts. Because both providers implement the same interface, the same rendering and business logic runs identically in workspace dev and on a deployed registry, see Architecture → The DATABASE_URL switch and Workspace → Local preview for the workspace-side view of this same switch.

What this replaces

Earlier handoff-app versions materialized the entire Next.js app into each client project: a prepare-runtime/vercel-build step copied the app into .handoff/runtime/, customized it via handoff.config.* hooks, symlinked node_modules from a parent location, and deployed that generated tree to Vercel. That model ran into structural problems ADR-001 documents in detail; worth calling out the two biggest:

  • Symlink hostility. Vercel's serverless packager rejects deployments containing symlinked directories, and the runtime's node_modules symlink (needed to avoid a second, conflicting copy of next/react) was unavoidable in that model.
  • Infrastructure/data mismatch. The Next.js app, auth, MCP, push/pull APIs, component preview iframes, admin UI, is infrastructure. Per-project content (components, pages, tokens, theme, navigation) is data. Materializing per client forced every content change to redeploy infrastructure.

The old commands (prepare-runtime, vercel-build, init:vercel, the materialization_layout config) still work for legacy deployments that haven't migrated, but are not the path for a new registry, see CLI reference → Deprecated.

Why this matters day to day

Because the same handoff-app codebase runs both roles, there is no separate "registry build" to maintain, a bug fix or feature ships to every registry the same way (merge to the tracked branch, redeploy), and a client's content update never requires a code deploy at all, just handoff-app push:all.

On this page