Register providers and services
The Services container is your host’s composition root. It is where shared
capabilities are constructed, where every provider is registered, and where the surfaces
(HTTP, eval) reach in to find what to expose. This guide covers configuring the container,
registering one or several providers, the validation that happens when you do, and how those
providers are aggregated for the eval endpoint.
Create the container
Section titled “Create the container”import { Services } from '@grundlag/core';import { createFileSystemCreator } from '@grundlag/core/filesystem';import { createPgliteCreator } from '@grundlag/core/pglite';
const services = new Services({ databaseCreator: createPgliteCreator((id) => `./data/db/${id}`), bucketCreator: createFileSystemCreator((id) => `./data/buckets/${id}`),});The databaseCreator tells providers how to obtain their local databases. Providers that
persist state ask the container for a database by id; createPgliteCreator resolves each id
to a directory holding an embedded Postgres. The bucketCreator does the same for file blobs — see
Store blobs in a bucket. Providers that need
neither simply never ask, and a container given neither creator falls back to in-memory
storage for both. See
Services and dependency injection for the container’s
resolution model.
Get the provider registry
Section titled “Get the provider registry”Providers live in the ProviderRegistry, which you resolve from the container:
import { ProviderRegistry } from '@grundlag/core';
const providers = services.get(ProviderRegistry);services.get is how you reach any shared capability. The registry it returns is the list
of providers this host serves.
Register a provider
Section titled “Register a provider”A provider package exports a provider type — a factory with a config schema. You register it with the configuration that type expects:
import { notesProvider } from '@acme/notes-provider';
await providers.register(notesProvider, { workspace: 'personal',});register is asynchronous because setting a provider up may open connections or run
migrations. Once it resolves, the provider’s actions and entities are live on every surface.
Configuration is validated
Section titled “Configuration is validated”The second argument is parsed against the provider type’s own Zod config schema before
the provider is set up. If it does not match, register throws a
ProviderConfigValidationError carrying the offending providerId and the Zod issues — the
provider is never constructed with bad configuration.
try { await providers.register(notesProvider, { workspace: 42 });} catch (err) { // ProviderConfigValidationError: config for `notes` did not match its schema}Configure providers from environment variables when the values are secrets or deployment-specific, and register conditionally when a provider is optional:
const { NOTES_TOKEN } = process.env;if (NOTES_TOKEN) { await providers.register(notesProvider, { token: NOTES_TOKEN });}Register several providers
Section titled “Register several providers”A host can serve as many providers as you like. Register each one — the registry keeps them distinct by id, and each provider’s actions and entities are namespaced by that id on the HTTP API.
import { notesProvider } from '@acme/notes-provider';import { calendarProvider } from '@acme/calendar-provider';
await providers.register(notesProvider, { workspace: 'personal' });await providers.register(calendarProvider, { timezone: 'Europe/Copenhagen' });The HTTP surface exposes them under distinct prefixes — /api/providers/notes/... and
/api/providers/calendar/... — with no coupling between them.
How actions are aggregated
Section titled “How actions are aggregated”The registry also flattens every registered provider’s actions into one map, via
toActions(). This is the single shared action layer the non-HTTP surfaces build on. The
eval endpoint calls it at startup:
const actions = await providers.toActions();// { 'notes.create': Action, 'notes.search': Action, 'calendar.createEvent': Action, ... }Each provider contributes its actions under its own namespace, so ids stay unambiguous across providers. The eval endpoint hands this aggregated map to a sandbox, which is why a script can call actions from any registered provider. See The eval endpoint and MCP.
Where this sits
Section titled “Where this sits”Everything above happens in your host entry file, before createServer. The container you
built is the one you pass to createServer({ services }) — the surfaces read their providers
from it. See Compose your own host for the full entry file,
and Providers and registries for the registry model.