@bubblelab/cli
v0.5.0
Published
bl — the BubbleLab CLI for serverless folders: pull a folder to files, work in a session with hot reload, run flows per environment, release through CI, and roll back.
Downloads
1,190
Maintainers
Readme
bl — serverless folders
A folder in serverless mode is owned by a git repo. Its flows' code and its environment configuration live in files; BubbleLab becomes the deploy target rather than the editing surface.
The rule everything else serves:
A change reaches a production environment only by becoming a release, and a release needs the release key that lives only in CI — i.e. only after a pull request merged.
Install
npm i -g @bubblelab/cli # needs Node 20+
bl login # paste an API keybl checks the npm registry for a newer version at most once a day (1.5s
budget, cached in ~/.bubblelab/update-check.json, warning on stderr). It
never blocks or fails a command; set BL_SKIP_UPDATE_CHECK=1 to disable.
Authentication
bl uses the same per-user API keys as the MCP server — the bl_mcp_… keys
from Settings → API keys. One key to issue, scope and revoke, whichever client
you point at it.
bl login --key bl_mcp_… --server https://api.bubblelab.ai
bl logout # forget the stored keyKeys resolve in this order, first hit wins:
--keyon the command line (handy in CI)$BUBBLELAB_API_KEY~/.bubblelab/credentials.json, keyed by server URL — whatbl loginwrites, owner-readable only
The key's access level is enforced server-side, so it means the same thing here as it does for MCP:
| Level | Can |
| --------- | ----------------------------------------------------------- |
| read | pull, status |
| write | …plus adopt, version new, push, release, rollback |
| execute | …plus run |
A key that is too narrow gets a 403 insufficient_access naming the level it
would need. Releasing additionally requires the release secret key
(--sk / $BUBBLELAB_RELEASE_SK), which lives only in CI.
The release key
There is nothing to fetch: it is a symmetric secret you generate once and set
in two places. The server compares what CI sends against its own
BUBBLELAB_RELEASE_SK, so the only requirement is that both hold the same
string.
openssl rand -base64 32 # generate it
gh secret set BUBBLELAB_RELEASE_SK # …then set the SAME value on the API serverUse a different key per environment — a staging key that also opens production
defeats the point, since holding this key is the whole reason the pull-request
gate means anything. A server with the variable unset refuses to release at all
(Release is not configured on this server), which is the right default for
anything you never intend to release to.
templates/release.yml in this package is a ready-made GitHub Actions workflow
that releases on merge to main. Copy it to .github/workflows/release.yml in
the repo holding your flows.
The loop
bl adopt --folder 49 --server https://api.bubblelab.ai # once, per folder
bl pull # write the folder to files
git add . && git commit -m "sync ogc-dashboard" && git pushFrom then on a teammate needs exactly two commands:
git clone …/ogc-dashboard && cd ogc-dashboard
bl dev # detects the folder, reserves a session, watches, hot-pushesbl dev reads the committed bubblelab.json, checks your access, reserves a
session if this clone has none, and pushes on save. A clone that already matches
the active release starts with zero pushes; a stale clone is flagged rather than
silently pushed.
push, run, sync — which one?
Default to bl push + bl run <slug> --env dev: the run targets YOUR isolated
session, so push + run is a complete test loop — nobody else sees a thing.
bl sync is different in kind, not degree: it writes the SHARED dev live
plane — what every teammate and the dev crons/triggers serve immediately. You
need it ONLY when a real cron, event trigger or inbound webhook must physically
fire on dev. It names its blast radius and confirms before writing (--yes
skips, and is required when stdin is not a TTY, e.g. CI). Prod is never touched
either way — it pins to releases.
Point a frontend at "my current session"
Session names churn — every release deletes them. Your frontend should never chase one. Instead:
bl pointer # → dv_3f9c… (also printed by bl dev)Write ?version=dv_3f9c… into your dev config once. It resolves at request
time to your newest open session on this folder, falls back to released code
when you have none, survives releases, and works on keyless webhooks. It is
read-only — it can select a snapshot in non-protected envs, never push or
release, and prod still answers 403. Revoke a leaked one with
DELETE /folders/:id/serverless/dev-pointer; the next bl pointer mints fresh.
From the CLI or MCP, --version @me does the same thing using your own
authentication: bl run list-users --env dev --version @me.
Sessions self-heal. When a release sweeps your session mid-bl dev, the
next save quietly opens a fresh one and carries on — and the pointer follows it.
Break-glass env edits
Environment rows stay readable and editable in the dashboard (rotate a
credential, tweak a variable at 2am) and apply immediately. The next release
re-applies environments/*.json from the repo, so run bl pull afterwards —
the default pull exports the LIVE env rows, letting your fix round-trip into
git. Pulling a specific release (bl pull --version v3) still gives that
release's stored config.
Layout
ogc-dashboard/
├─ bubblelab.json committed — folder id + server. This is what makes the repo shareable.
├─ configuration.json committed — flow inventory
├─ environments/
│ ├─ prod.json committed — $cred ids, variables, trigger config. No secrets.
│ └─ dev.json
├─ list-users/
│ └─ flow.ts the directory name IS the slug, and the webhook path
└─ .bubblelab/ gitignored — this clone's session id and content hashesCredentials are referenced by id ({"$cred": 2760}) because ids survive both
rename and rotation. Secret-classified variables are references too
({"$secret": "NAME"}); no secret value ever leaves the server.
Commands
| Command | Does |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| bl adopt --folder <id> | Enable serverless mode. Snapshots today's live state as v1 and pins protected envs to it — a bookkeeping event, not a deploy. |
| bl pull [--version v2] | Write the folder to files. Refuses to run when you have unpushed edits (--force discards them). |
| bl version / bl version new | Show this clone's session, or reserve one. |
| bl push | Push the working tree into the session. Idempotent by content hash. |
| bl sync [--yes] | Write the tree onto the SHARED dev live plane. Confirms first; never touches prod. See "push, run, sync". |
| bl dev | Auto-session + watch + hot push. |
| bl run <slug> --env dev | Run a flow. --env is required. |
| bl status | Local vs session vs active release; shouts if prod is rolled back. |
| bl release --sk <key> | Release the tree. CI only in practice. |
| bl rollback --env prod --to 1 | Repoint an environment at an earlier release. |
Rules worth knowing
- Protected environments are recognized by name: anything containing
prodruns released versions only. Passing?version=<session>to a production URL is a403, not a shortcut. - Sessions are trigger-inert. Cron and webhook activation happen at release time, so open sessions add zero scheduled jobs however many there are.
- Direct edits are locked while a folder is serverless — Studio save, MCP
edit-flow, slug rename,set-credentials— so database truth cannot fork from git truth. - Rollback restores configuration too, not just code, and says so in
bl statusuntil a revert merges.
Tests
bun test # arg safety, offline — runs in CI
bun test/acceptance.ts # 53 criteria, real API, real Clerk credentials
bun demo/record.ts # re-record the demo transcript
bash demo/build-video.sh # rebuild the demo video from itThe acceptance suite starts its own API on :4103 (it needs the release key in
the server process), builds a clerk-ops fixture, and cleans up after itself.
Its offline counterpart — the invariants, without live credentials — runs in CI
as apps/bubblelab-api/src/test/serverless-folder-mode.test.ts.
