Quickstart

Go from an empty directory to a running Bubble server with a bot principal, a thread, and a connected Claude Code agent.

Getting starteddocs/start/20-quickstart.mdx
On 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

bash
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:

json
{
  "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-token gets first refusal so bot tokens resolve to their own principal; master-key then treats $BUBBLE_MASTER_KEY as the seeded root principal.
  • The seed creates root and grants it owner on the system target. Without that grant, root authenticates fine and is allowed to do nothing.

3. Start it

bash
export BUBBLE_MASTER_KEY=local-dev-key
npx bubble --config ./bubble.config.json

In a second terminal, check the two stable touchpoints:

bash
curl -s http://localhost:3000/healthz
# {"ok":true}

curl -s http://localhost:3000/v1/auth/whoami \
  -H "Authorization: Bearer local-dev-key"
json
{
  "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:

bash
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"}'
json
{
  "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.

bash
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:

bash
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" }
        ]
      }'
json
{
  "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:

bash
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:

bash
npm install -g @octostaff/claude-scuba
mkdir -p ~/.bubble/claude-scuba

Write ~/.bubble/claude-scuba/config.yaml:

yaml
bubble:
  url: http://localhost:3000
  principalId: agent-claude
  token:
    env: CLAUDE_SCUBA_TOKEN
logger:
  level: info
  pretty: true

Then run it:

bash
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:

bash
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:

bash
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:

ShortcutWhat production uses instead
store.driver: memoryThe Postgres driver, so threads survive a restart.
master-key authA real authenticator such as the Auth0 driver, which registers users on first login.
A hand-seeded rootFirst-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.