Compose your own host
A host is an application you write. The platform’s goal is a shared capability layer:
you describe what your systems can do once, as providers, and then expose that one layer
through many surfaces — a typed HTTP API, an eval endpoint for scripting, and MCP for agent
integration. @grundlag/server ships the building block for the HTTP surface,
createServer; it does not ship a fixed deployment. Your entry file is where you assemble
your providers and turn them on.
This guide walks through that entry file end to end.
Install
Section titled “Install”npm install @grundlag/server @grundlag/coreAdd whichever provider packages you intend to serve. Throughout this guide we use a
hypothetical notes provider, @acme/notes-provider.
Write the entry file
Section titled “Write the entry file”The whole host is four steps: create a Services container, register your providers into
its ProviderRegistry, build the server, and listen.
import { ProviderRegistry, Services } from '@grundlag/core';import { createFileSystemCreator } from '@grundlag/core/filesystem';import { createPgliteCreator } from '@grundlag/core/pglite';import { createServer } from '@grundlag/server';import { notesProvider } from '@acme/notes-provider';
// 1. The composition root. Every shared capability lives here.const services = new Services({ databaseCreator: createPgliteCreator((id) => `./data/db/${id}`), bucketCreator: createFileSystemCreator((id) => `./data/buckets/${id}`),});
// 2. Register the providers this host should serve.const providers = services.get(ProviderRegistry);await providers.register(notesProvider, { // Configuration validated by the provider's own Zod schema. workspace: 'personal',});
// 3. Build the Fastify app around those providers.const server = await createServer({ services });
// 4. Serve.await server.listen({ port: 3800 });That is a complete host. Every action and entity on every registered provider is now a
validated HTTP endpoint, the eval endpoint is live, and interactive docs are served at
/api/docs.
The Services container
Section titled “The Services container”Services is the host’s dependency-injection container — the single place shared
capabilities are constructed and looked up. The one option most hosts pass is a
databaseCreator, which tells providers how to obtain their local databases.
createPgliteCreator (from @grundlag/core/pglite) maps a database id to a directory
holding an embedded Postgres. A host whose providers store file blobs passes a
bucketCreator too:
const services = new Services({ databaseCreator: createPgliteCreator((id) => `./data/db/${id}`), bucketCreator: createFileSystemCreator((id) => `./data/buckets/${id}`),});Each provider gets its own database directory and its own bucket directory, named by id,
under ./data. Omit a creator and that storage stays in memory for the process’s lifetime. See
Register providers and services for how the
container aggregates providers, and Services and dependency injection
for the container itself.
Embedded or hosted
Section titled “Embedded or hosted”createPgliteCreator runs Postgres in-process, compiled to WASM — no server to operate and
no native module to build. createPostgresCreator (from @grundlag/core/postgres) talks to
a real server instead, giving each database id its own schema:
import { createPostgresCreator } from '@grundlag/core/postgres';
const services = new Services({ databaseCreator: createPostgresCreator({ connectionString: process.env.DATABASE_URL }),});Both are Postgres, so they share a dialect and a feature set. Moving a deployment from embedded to hosted changes these two lines and nothing inside a provider — which is the point of picking one engine rather than one per environment.
A creator is expected to provide vector storage. createPgliteCreator loads pgvector
itself; on a hosted server the extension has to be installed, which on a managed service is
usually a checkbox or a create extension a DBA runs. A provider that stores embeddings will
say so plainly when it is missing rather than degrading quietly.
createServer options
Section titled “createServer options”createServer accepts a single optional CreateAppOptions object and returns a Fastify
instance.
createServer(options?: CreateAppOptions): Promise<FastifyInstance>;| Option | Type | Default | Description |
|---|---|---|---|
services |
Services |
a new empty Services |
The container whose registered providers are exposed. Pass the one you configured. |
logger |
boolean |
undefined |
Enables Fastify’s built-in logger when true. |
If you omit services, createServer builds an empty container and the host serves no
providers — so in practice you always pass your own.
const server = await createServer({ services, logger: true });Because it returns a plain Fastify instance, you can register your own plugins, hooks, or
routes on it before calling listen, and you control the listen options (port, host).
Verify it is running
Section titled “Verify it is running”With the server listening on port 3800:
curl http://localhost:3800/api/health# {"status":"ok"}Open http://localhost:3800/api/docs in a browser for the interactive Scalar/OpenAPI
reference — it lists every endpoint the host generated from your registered providers.
Next steps
Section titled “Next steps”- Register providers and services — the composition root in depth.
- Call the HTTP API — invoke actions and search entities.
- The eval endpoint and MCP — scripting over the shared action layer.
- HTTP API reference — the full endpoint contract.