Launching a server
Run a Bubble server with the bundled CLI, or embed it programmatically when you need more control.
docs/server/10-launch.mdxOn this page
Bubble ships a small bubble CLI that reads a JSON config file and starts
a wired Fastify server. For most use cases — local development, a
container CMD, a service unit — the CLI is all you need. The same
launcher is also exported as a function for callers that want to embed
Bubble inside a larger Node process.
The CLI
After installing @octostaff/bubble in a project (or running it via
pnpm exec / npx), the bubble binary is on the path. Pointing it at
the bundled dev config is a complete program:
export BUBBLE_MASTER_KEY=local-dev-key bubble --config ./examples/config/local-dev.json
That gives you:
- The in-memory store seeded with one
systemprincipal (root) that owns the system target. - A two-driver auth stack:
local-token(bot tokens) followed bymaster-key(yourBUBBLE_MASTER_KEYis interpreted asroot). - HTTP routes mounted under
/v1/..., a WebSocket endpoint at/v1/ws, and/healthzfor liveness.
Run with --help to see every option:
Usage: bubble [options]
Options:
-c, --config <path> Path to a JSON config file (default: $BUBBLE_CONFIG, or built-in defaults)
-p, --port <port> Port to listen on (default: $PORT or 3000)
-h, --host <host> Host to bind to (default: $HOST or 0.0.0.0)
--help Show this messageThe CLI:
- Resolves
--configagainst the current working directory and parses it as JSON. Without a flag it falls back to$BUBBLE_CONFIG; without that it launches with built-in defaults (in-memory store, singledefaultauthenticator, no seeded principals or grants — fine for a smoke test, not useful for real work). - Reads
--port/$PORTand--host/$HOSTfor the listen address. - Handles
SIGINTandSIGTERMby closing Fastify cleanly.
Watch Mode for Local Development
While iterating on Bubble itself, the package exposes a dev script that
rebuilds on every source change and restarts the CLI against the bundled
dev config:
export BUBBLE_MASTER_KEY=local-dev-key pnpm dev
That runs tsup --watch over src/ and, on each successful build,
re-launches bubble --config ./examples/config/local-dev.json. Stop it with
Ctrl+C.
Zero-Config (Sanity Check)
The shortest possible invocation skips the config file entirely:
bubble # server listening on http://0.0.0.0:3000
Every config field has a default, so this starts a server — but with no seeded principals or grants nothing is authorised to do anything interesting. Use it for a quick "did the binary install correctly?" check and reach for a config file as soon as you want to exercise the API.
Containerized / Service Use
The same binary is what you point a container or service unit at:
ENV BUBBLE_CONFIG=/etc/bubble/config.json CMD ["bubble"]
# systemd unit excerpt ExecStart=/usr/local/bin/bubble --config /etc/bubble/config.json Environment=PORT=8080
A Working Local Config
The package ships four runnable configs under examples/config/, each
for a different job:
| File | For |
|---|---|
reference.json | A documented template showing every driver and option. |
local-dev.json | What pnpm dev runs: in-memory store, master key, a seeded root owner. |
fixtures.json | The seeded corpus the integration tests and UI verification run against. |
entra.json | Signing in against a real Microsoft Entra ID directory. |
local-dev.json is the starting point:
{
"store": { "driver": "memory" },
"auth": [
{ "driver": "local-token" },
{
"driver": "master-key",
"masterKeyEnvVar": "BUBBLE_MASTER_KEY",
"principalIdHeader": "x-bubble-principal-id",
"defaultPrincipalId": "root"
}
],
"seed": {
"principals": [{ "principalId": "root", "kind": "system", "displayName": "Development Root" }],
"authz": [{ "target": { "type": "system" }, "principalId": "root", "role": "owner" }],
"threads": []
},
"maxEventPageSize": 100,
"logger": { "level": "info" }
}Copy it, set BUBBLE_MASTER_KEY in the environment, and bubble --config ./examples/config/local-dev.json is enough to exercise the whole API
as the root system owner.
Running Against a Real Entra ID Directory
examples/config/entra.json boots the same in-memory server with the
entra driver in front of it, so a browser can
complete a real Microsoft sign-in against a local server.
The directory identifiers come from the environment rather than the file, so a real tenant's ids never have to be written into a tracked config to run against it:
export BUBBLE_ENTRA_TENANT_ID=<directory (tenant) id> export BUBBLE_ENTRA_CLIENT_ID=<application (client) id> export BUBBLE_MASTER_KEY=dev pnpm --filter @octostaff/bubble dev:entra
The app registration needs an SPA redirect URI pointing at wherever Starfish is
served — http://localhost:3001/auth/entra/callback for pnpm dev — plus the
exposed API scope. The registration requirements are listed under the
entra driver.
The master-key driver is kept in that config on purpose: it is the way back in
as root if the federated login is not working yet, and the way to inspect what
a completed sign-in actually created.
Verifying It Is Up
Two stable touchpoints regardless of config:
# Liveness check; no auth required.
curl -s http://localhost:3000/healthz
# => {"ok":true}
# Calling identity for the credentials on this request.
curl -s http://localhost:3000/v1/auth/whoami \
-H "Authorization: Bearer $BUBBLE_MASTER_KEY"
# => { "principal": { "principalId": "root", "kind": "system", ... }, "grants": [...] }If whoami returns the expected principal and a non-empty grants array,
the auth stack and the seed both took effect. A 401 means the
authenticator stack rejected the credential; a successful response with
grants: [] means the authz seed did not match — double-check
seed.principals / seed.authz.
Embedding Programmatically
When the CLI does not fit — for example, you want to merge config from
multiple sources, attach extra Fastify plugins, or run Bubble alongside
other routes — call launchBubbleServer directly. The CLI is a thin
wrapper around exactly this entry point:
import { launchBubbleServer } from '@octostaff/bubble';
const app = await launchBubbleServer({
store: { driver: 'memory' },
auth: [{ driver: 'default', defaultPrincipalId: 'root' }],
seed: {
principals: [{ principalId: 'root', kind: 'system' }],
authz: [{ target: { type: 'system' }, principalId: 'root', role: 'owner' }],
},
});
await app.listen({ port: 3000, host: '0.0.0.0' });If you want to validate or inspect the parsed config before launch (for example, to overlay environment overrides on a file), call the parser explicitly:
import { launchBubbleServer, loadBubbleServerConfig } from '@octostaff/bubble';
const config = loadBubbleServerConfig(rawConfig);
// ... inspect or mutate `config` ...
const app = await launchBubbleServer(config);loadBubbleServerConfig only parses and defaults; the launcher builds
the runtime objects (store, authenticator, seeded principals, seeded
grants, policy, Fastify instance) from the parsed result.
What Each Config Field Drives
A short orientation map; each field is detailed in the linked doc:
| Field | Drives | Detail in |
|---|---|---|
store | Which BubbleStore implementation backs the server (memory or postgres). | Thread Model |
auth | Ordered authenticator stack used by the dispatcher. | Authentication |
seed.principals | Durable principals created at startup; each receives owner on itself. | Bootstrapping |
seed.authz | Grant rows seeded into the store after the principal seed. | Bootstrapping |
maxEventPageSize | Upper bound on getEventPage responses; honoured by the in-memory store today. | Events |
logger | Forwarded to Fastify (true, false, or a logger options object). | — |
publicBaseUrl | Bubble's externally reachable origin for extension-owned absolute URLs. | — |
extensions.<key> | Enables and configures one trusted local server extension. | — |
extensions.artifacts | Enables the built-in artifacts extension. | — |
Core Bubble does not depend on the artifact protocol. The artifact manifest,
role capabilities, catalog schema, routes, loopback API, and cleanup lifecycle
are enabled together only when extensions.artifacts is present. A local
durable deployment can pair the Postgres catalog with disk bytes:
{
"publicBaseUrl": "https://bubble.example.com",
"extensions": {
"artifacts": {
"signingSecret": { "env": "BUBBLE_ARTIFACT_SIGNING_SECRET" },
"objects": { "driver": "disk", "root": "/var/lib/bubble/artifacts" }
}
}
}When this block is absent, Bubble does not advertise artifacts from
GET /v1/info and does not mount /v1/artifacts. Unknown extension config keys
are rejected at startup rather than silently ignored.
Use objects.driver: "azure" with either connectionString or accountUrl
(Default Azure Credential). Set the optional objects.publicUrl to an HTTP(S)
origin such as https://files.example.com when direct upload and download SAS
URLs should use a custom domain. Bubble still authenticates and signs against
the canonical account endpoint, then replaces only the URL origin; the custom
endpoint must preserve the path and query string and support GET and PUT.
Where the storage account disallows shared-key and SAS access, pair
accountUrl with objects.transfers: "bubble". Bubble then never mints a SAS:
upload and download URLs point at Bubble's own signed transfer route, and Bubble
streams the bytes to and from the container with its Entra credential. Grant
that identity Storage Blob Data Contributor on the storage account.
Clients such as Starfish need to reach only Bubble, and every transfer's bytes
cross Bubble's network path. publicUrl applies only to the default
transfers: "direct".
{
"objects": {
"driver": "azure",
"container": "artifacts",
"accountUrl": "https://account.blob.core.windows.net",
"transfers": "bubble"
}
}Artifacts are governed by an explicit authorization target rather than by their
URL. The authenticated control plane lives under /v1/artifacts, and each
operation supplies its accessScope (sourceAccessScope and
destinationAccessScope for imports). SDK consumers register the trusted local
artifactClientExtension and obtain its API with
client.extensions.require(artifactExtensionKey). That call negotiates the
locally installed protocol version against /v1/info; Bubble never sends or
loads executable extension code. Amphibians can explicitly compose the
extension's tool provider to expose the same authenticated API to a model.
Bubble has no other way to bootstrap authorization: the policy reads from
the store, the store starts empty, so a useful server needs at least one
seed.principals entry plus a matching seed.authz grant (or, in
production, a real authenticator that registers users on first login).
Mount Points
Once the server is listening, everything Bubble exposes lives under two prefixes:
/v1/...— the HTTP REST surface. Full route catalogue in HTTP Protocol./v1/ws— the multiplexed WebSocket endpoint. Frame contracts in WebSocket Protocol.
Plus the version-independent /healthz for liveness probes.
Next Steps
- If you want to understand the auth stack you just configured, jump to Authentication.
- If you are wiring this into an embedding application that already parses config from YAML/env, Bootstrapping walks through the seeding and registration flow that turns authenticated requests into durable principal rows.