Home / Docs / Surfaces
Surfaces
A surface is an edge — an extension that plugs into an integration, or into the core engine itself. It's how Yggdrasil grows new reach. A surface does not have to be a UI: it can be a dashboard, an API, an MCP server, a bot, a webhook edge — any shape. Each surface rides the platform session and keeps no business state of its own.
An edge, not a frontend
Think of a surface as a plug. An integration knows how to talk to one external system; the core engine holds the manifests, runs and authority. A surface attaches to one of those — extending an integration with a new face, or extending the core itself — and turns that reach into something people or machines can use.
Because it's defined by where it attaches and not by what it looks like, a surface can be human-facing or machine-facing. A health dashboard for a Stripe integration, a read-only REST API over run history, an MCP server that exposes workflows to an LLM, a Slack bot that dispatches a deploy — all of these are surfaces. What makes something a surface is not its form; it's the set of invariants below.
The slime model
If an integration is Yggdrasil's rigid spine (idempotent provisioning, strict naming convention), a surface is the slime that molds around it. The shape is deliberately free-form: a dashboard, an onboarding wizard, a REST/gRPC API, an MCP server for LLM tooling, a Slack bot, a webhook edge, an iframe-embedded widget — or a hybrid of all of these. What makes something a surface isn't the shape, it's the set of invariants below.
The slime also adapts to the operator's stack, not the other way around: a surface doesn't assume which cloud hosts it, which CDN serves its assets, or which UI framework builds it (React, Vue, Svelte, Go server-rendered — all valid). That's the Lego principle applied to the edges.
The invariants
Even the most custom, strangest surface honors these six invariants. Failing any one of them means it isn't a Yggdrasil surface (it may be a great web app, an API or a bot — just not a surface):
- Stateless as to business state — a surface doesn't own business state. Session-level UX state (form drafts, expanded panels, an observation cache) is fine; business state lives in the integrations and backends.
- Auth via the platform session (delegated) — never reimplement login from scratch. Public surfaces use reduced-scope, read-only platform tokens; private ones use the full session.
- Backend-agnostic (via the core) — it calls
/api/v1/*to read data and dispatches workflows for mutations. It doesn't talk directly to Stripe/AWS/GitHub — that goes through the integrations. - Multi-tenant aware — it respects the
integration_instancescope. Operator Acme sees Acme's resources; Globex sees Globex's. Cross-tenant visibility requires an explicit RBAC grant. - Federable (deployable) — it's a standalone container/binary that plugs into the console (or runs standalone for public variants). It doesn't need any central codebase to build.
- No business authority — a surface dispatches, visualizes, composes. It does not decide business rules ("who to charge", "when to refund", "is action X allowed"). Those decisions live in the integrations, the workflows or backends with the proper authority.
Console & design system
The console is the reference surface: a React 19 + Vite SPA that renders the core's API for human operators — collaborators, teams, integrations, workflows and identity. Every read and write goes through /api/v1/*; the session and all authority live in the core. You can fork it, restyle it, or swap the whole console for another UI pointed at the same core — the platform doesn't change.
The console aggregates the other surfaces, and each integration can publish its own (one panel per integration). What keeps them all looking the same is the surface-toolkit — the shared React design system. It delivers the three things every surface needs:
Tokens & theme
The SurfaceThemeProvider and the tokens (colors, spacing, typography) ensure every surface shares a single look.
IntegrationAdminShell
The shell that handles routing, tab layout, the instance picker and the loading/empty/error states — the author only writes the body of the tabs. There's also TeamContextShell, centered on a team.
Data hooks
React Query hooks (useYggdrasilAPI, useInstance, useDriftStatus, useSurfaceQuery…) that talk to the core through the session, plus ready-made tabs (DriftTab, IdentitiesTab, RecentRunsTab).
Every hook goes through useYggdrasilAPI, which does fetch with credentials: "include" against the /api/v1 base — surfaces never reimplement auth, they ride Yggdrasil's delegated session.
Create your own
The recommended path is the CLI, which clones the surface-template (the canonical scaffold), discards the git history, rewrites the module path to github.com/<org>/surface-<name> and runs git init. The result already compiles and go test ./... passes.
$ yggdrasil new surface webhook-health --owner acme
✓ scaffold ready
directory: ./surface-webhook-health
module: github.com/acme/surface-webhook-health
# the template compiles out of the box; go test ./... passes
If you work inside a workspace checkout and want the surface installed under surfaces/, there are workspace helpers — and the surfaces subcommand manages the active set:
$ yggdrasil surfaces list # surfaces available in the catalog
$ yggdrasil surfaces scaffold webhook-health # creates under surfaces/
$ yggdrasil surfaces activate webhook-health # adds it to the active compose
$ yggdrasil surfaces active # lists the currently active ones
The scaffold registers a surface.manifest.json (kind: surface) that declares the category, runtime, the core_contracts the surface consumes and its capabilities — and serves /, /healthz and /readyz out of the box. From there you add your handlers (or mount the toolkit's IntegrationAdminShell) and publish the image.