Quickstart
Go from an empty directory to a running Bubble server with a bot principal, a thread, and a connected Claude Code agent.
docs/start/20-quickstart.mdxOn this page
This walkthrough starts with nothing and ends with an agent answering in a thread. It uses an in-memory store and a master key, so everything here is local-development shaped — see where to go from here for what changes in production.
You need Node.js 22 or newer and npm.
1. Install the server
mkdir octostaff-quickstart && cd octostaff-quickstart npm init -y npm install @octostaff/bubble
That puts the bubble binary on the local path. npx bubble --help confirms
the install.
2. Write a config
Bubble has no useful defaults on purpose: the policy reads from the store, and the store starts empty, so a server with no seed authorizes nobody to do anything. The smallest config that gets you a working operator identity is:
{
"store": { "driver": "memory" },
"auth": [
{ "driver": "local-token" },
{ "driver": "master-key", "masterKeyEnvVar": "BUBBLE_MASTER_KEY", "defaultPrincipalId": "root" }
],
"seed": {
"principals": [{ "principalId": "root", "kind": "system", "displayName": "Local Root" }],
"authz": [{ "target": { "type": "system" }, "principalId": "root", "role": "owner" }]
},
"logger": { "level": "warn" }
}Save it as bubble.config.json. Three things are doing work:
store.driver: "memory"keeps everything in process. Restarting the server discards every thread — which is what you want while experimenting.- The auth stack is ordered.
local-tokengets first refusal so bot tokens resolve to their own principal;master-keythen treats$BUBBLE_MASTER_KEYas the seededrootprincipal. - The seed creates
rootand grants itowneron the system target. Without that grant,rootauthenticates fine and is allowed to do nothing.
3. Start it
export BUBBLE_MASTER_KEY=local-dev-key npx bubble --config ./bubble.config.json
In a second terminal, check the two stable touchpoints:
curl -s http://localhost:3000/healthz
# {"ok":true}
curl -s http://localhost:3000/v1/auth/whoami \
-H "Authorization: Bearer local-dev-key"{
"principal": { "principalId": "root", "kind": "system", "displayName": "Local Root" },
"grants": [
{ "target": { "type": "principal", "principalId": "root" }, "role": "owner" },
{ "target": { "type": "system" }, "role": "owner" }
]
}A 401 means the auth stack rejected the credential. A 200 with grants: []
means the seed did not take — check that seed.authz names the same
principalId as seed.principals.
4. Create a bot principal
Agents join as bot principals, and bots are the one kind created through an
explicit route rather than seeded from config. Creating one also issues its
first token:
curl -s -X POST http://localhost:3000/v1/principals \
-H "Authorization: Bearer local-dev-key" \
-H "Content-Type: application/json" \
-d '{"principalId":"agent-claude","kind":"bot","displayName":"Claude Code"}'{
"principal": { "principalId": "agent-claude", "kind": "bot", "displayName": "Claude Code" },
"token": {
"tokenId": "db2b7c13-8cd9-4cf0-bb32-8094cf8f238b",
"principalId": "agent-claude",
"token": "bubble_agent_…"
}
}Copy the token value now. Bubble stores only a hash of it, so this
response is the only time the plaintext exists. If you lose it, issue another
with POST /v1/principals/agent-claude/tokens.
export CLAUDE_SCUBA_TOKEN='bubble_agent_…'
The call also grants agent-claude the principal_manager role on itself —
narrower than the owner a user gets, because a bot may manage its own profile
and tokens but may not create further principals.
5. Open a thread
A thread is created with its participants in the same call:
curl -s -X POST http://localhost:3000/v1/threads \
-H "Authorization: Bearer local-dev-key" \
-H "Content-Type: application/json" \
-d '{
"threadId": "thread-hello",
"participants": [
{ "principalId": "root", "role": "member" },
{ "principalId": "agent-claude", "role": "member" }
]
}'{
"threadId": "thread-hello",
"headSeq": 2,
"participants": [
{ "principalId": "root", "role": "member" },
{ "principalId": "agent-claude", "role": "member" }
]
}headSeq is already 2 because seeding participants appends a
participant_joined event for each of them — the thread's membership is part
of its log, not metadata beside it.
Confirm the bot inherited what it needs by asking as the bot:
curl -s http://localhost:3000/v1/auth/whoami \ -H "Authorization: Bearer $CLAUDE_SCUBA_TOKEN"
Its grants should list principal_manager on its own principal target and
member on thread-hello. That member role is what lets a connector
subscribe and append.
6. Connect an agent
Install the Claude Code connector and point it at the thread's server:
npm install -g @octostaff/claude-scuba mkdir -p ~/.bubble/claude-scuba
Write ~/.bubble/claude-scuba/config.yaml:
bubble:
url: http://localhost:3000
principalId: agent-claude
token:
env: CLAUDE_SCUBA_TOKEN
logger:
level: info
pretty: trueThen run it:
claude-scuba
The connector authenticates, discovers every thread its principal has joined,
and subscribes. It does not need to be told about thread-hello — membership
is the discovery mechanism.
How the agent reaches a model is the Claude Agent SDK's business, not the
connector's; claude-scuba models no API key of its own. An ordinary Claude
Code login on the same machine is enough. The package ships
config.example.yaml
with every option filled in.
7. Say something
Anything that can append to the thread will now reach the agent. The blunt version is a pair of curl calls — a run envelope around one message:
curl -s -X POST http://localhost:3000/v1/threads/thread-hello/events \
-H "Authorization: Bearer local-dev-key" \
-H "Content-Type: application/json" \
-d '{"type":"run_started","principalId":"root","runId":"run-1","kind":"user_input"}'Read the thread back to watch what the agent appended:
curl -s http://localhost:3000/v1/threads/thread-hello/events \ -H "Authorization: Bearer local-dev-key"
In practice you would drive this from a UI rather than by hand — Starfish is the one that ships with the platform — or from the typed client in the SDK.
Where to go from here
Three things in this walkthrough are deliberately local-development shortcuts:
| Shortcut | What production uses instead |
|---|---|
store.driver: memory | The Postgres driver, so threads survive a restart. |
master-key auth | A real authenticator such as the Auth0 driver, which registers users on first login. |
A hand-seeded root | First-user promotion, which grants the first registered human owner on the system. |
- Launching a server — every config field, embedding the launcher, and running it as a container or service.
- Authentication — the driver stack you just wrote, explained end to end.
- Bootstrapping — how an empty store reaches a working system, including the first-user promotion.
- Running a Claude Code bot — the full connector configuration, backlog and resume policy, and Docker.