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:
handoff-appis deployed once per registry, unmodified. It'snpm install && npm run build:registryfrom thehandoff-apprepository root, no per-client customization, no materialization step. See Deploy.- 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.
- A workspace never deploys itself. It's a local git repo (components, pages,
theme.css,handoff.config.*, adesign-system/token pipeline) driven by thehandoff-appCLI. It pushes to a registry over HTTP; it never runs its own production deployment. - Single registry, single project, for now. One registry deployment serves one workspace
(one
figma_project_id). Multi-tenancy (anorg_idcolumn, 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_modulessymlink (needed to avoid a second, conflicting copy ofnext/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.