Skip to content

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.

Terminal window
npm install @grundlag/server @grundlag/core

Add whichever provider packages you intend to serve. Throughout this guide we use a hypothetical notes provider, @acme/notes-provider.

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.

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.

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 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).

With the server listening on port 3800:

Terminal window
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.