Skip to content

Deploy ​

By the end, Switchboard runs in production on Cloudflare: the bot as a container with a state Worker beside it, deployed from your terminal in one command.

You need: a Cloudflare account with a domain (a zone) in it, an API token for it in CLOUDFLARE_API_TOKEN (scopes: Set up accounts; Containers Edit is what the image copy needs), and the Slack tokens and provider key from Get started. No Docker, no clone.

The pieces ​

WorkerWhat it isNeeded?
memory (the state Worker)Durable Objects: config, run history, memory, schedules, overridesyes: without it nothing survives a restart
botThe always-on container: Slack, the model, the dispatcher, the dashboardsyes
residentOne always-warm container per onboarded repositoryoptional
sandboxA container per thread for toolsoptional

deploy all deploys them in that order.

Write the profile ​

bash
npx --yes @coreplane/switchboard@<version> init --organization <org> --anthropic-key <key> --slack-app-token <xapp-token> --slack-bot-token <xoxb-token> --cloudflare <account id> --zone <zone>

You should see:

wrote to /Users/you/.switchboard:
  .env                  (mode 600)
  config/config.yaml
  deploy/profile.json
Worker configs from deploy/profile.json:
  written   .switchboard/deploy/cloudflare-memory/wrangler.jsonc
  written   .switchboard/deploy/cloudflare/wrangler.jsonc
  …

The installation is ~/.switchboard (or SWITCHBOARD_HOME, or the directory you run in when it already holds one); every deploy command finds it from anywhere, and its .switchboard/ holds the rendered Worker configs. The profile's fields:

FieldMeaning
account, zoneYour account id; the zone every hostname is under.
workers.<name>script and hostname per Worker; delete one and the plan has no step for it.
imagesregistry (what init writes): the release's published images, copied into your account's registry by deploy all over HTTPS. The bot uses a release tag; resident and sandbox images are reused while their Dockerfile and copied files stay unchanged. build: each Dockerfile, built where deploy all runs (a checkout; needs Docker).
configSourceThe bot's config: a path, github://owner/repo/path@ref (needs CONFIG_REPO_TOKEN) or op://Vault/Item/field.
secretsSourceA directory of <NAME> files (~/.secrets/switchboard when absent) or op://Vault/Item.
access{ "teamDomain": "<team>.cloudflareaccess.com", "aud": "<AUD tag>" }: Cloudflare Access in front of the dashboards.

Point the config at the state Worker ​

In config/config.yaml:

yaml
runHistory:
  worker:
    baseUrl: https://switchboard-memory.<zone>
runtimeOverrides:
  worker:
    baseUrl: https://switchboard-memory.<zone>

Stage the secrets ​

deploy/secrets.manifest.json names every secret and its Workers. Values live at secretsSource, one file per name:

bash
mkdir -p -m 700 ~/.secrets/switchboard
openssl rand -hex 32 > ~/.secrets/switchboard/MEMORY_TOKEN

Add SLACK_BOT_TOKEN, SLACK_APP_TOKEN and ANTHROPIC_API_KEY the same way. Then:

bash
npx --yes @coreplane/switchboard@<version> deploy secrets memory
npx --yes @coreplane/switchboard@<version> deploy secrets bot
  • An absent required value refuses before any upload; an absent optional one is skipped, by name. Nothing is printed.
  • A shared bearer (MEMORY_TOKEN, SANDBOX_TOKEN, RESIDENT_*_TOKEN) carries one value on every Worker listed for it. --only NAME puts a subset.
  • --source op://Vault/Item reads from one 1Password item for this command only; combine it with --only NAME when that item holds one secret. The profile's source for other secrets stays the same.

Deploy ​

bash
npx --yes @coreplane/switchboard@<version> deploy plan --only memory,bot
MEMORY_TOKEN="$(cat ~/.secrets/switchboard/MEMORY_TOKEN)" npx --yes @coreplane/switchboard@<version> deploy all --only memory,bot

deploy plan executes nothing; its Images: line reads 0 of 1 present; deploy all copies the rest, or not probed (<why>) without a token. deploy all checks the account, copies the missing images into your registry, deploys the state Worker, pushes your config to it, deploys the bot, and waits until /healthz answers from the new container.

The state Worker's canonical config must be readable before a Bot-selected deployment starts. For the first installation, deploy Memory with deploy all --only memory, then deploy Bot. An unreadable endpoint is a refusal, not evidence that no config exists.

You should see:

copied into the account registry: bot …
[deploy:all] bot: live (commit <sha>; 45s after the upload)
deployed and live
FlagDoes
--only bot,resident · --skip sandboxSelect Workers.
--dry-runPrint the plan and what it would copy; copy and deploy nothing.
--affectedOnly the Workers whose inputs changed since the commit each one serves.
--forceBypass the bot and resident preflights.

Read from the environment: CLOUDFLARE_API_TOKEN (the registry read and the copy); MEMORY_TOKEN for the bot step; RESIDENT_READ_TOKEN for the resident; SANDBOX_TOKEN for the sandbox.

Add the optional Workers ​

WorkerConfig blockSecrets (shared with the bot)
residentexecution.resident.baseUrlRESIDENT_OPERATOR_TOKEN, RESIDENT_ADMIN_TOKEN; the GitHub App triple
sandboxexecution.type: cloudflare, execution.urlSANDBOX_TOKEN

From the package the profile already names them; from a checkout add the two workers entries. Mint the bearers, deploy secrets <worker> for each, then deploy all without --only. Repositories are onboarded from chat: @switchboard repo onboard acme/api.

Deploy from your CI ​

Optional: the same deploy from GitHub Actions. Commit deploy/profile.json and config/config.yaml to a repository of yours and add:

yaml
# .github/workflows/deploy-switchboard.yml
name: deploy switchboard
on:
  workflow_dispatch:
    inputs:
      targets:
        default: affected
permissions:
  contents: read
jobs:
  deploy:
    uses: <owner>/<repo>/.github/workflows/deploy-production.yml@v<version>
    permissions:
      contents: read
    with:
      cli: package
      version: <version>
      targets: ${{ inputs.targets || 'affected' }}
    secrets:
      CLOUDFLARE_DEPLOY_TOKEN: ${{ secrets.CLOUDFLARE_DEPLOY_TOKEN }}
      MEMORY_TOKEN: ${{ secrets.MEMORY_TOKEN }}
      RESIDENT_READ_TOKEN: ${{ secrets.RESIDENT_READ_TOKEN }}
      SANDBOX_TOKEN: ${{ secrets.SANDBOX_TOKEN }}

<owner>/<repo> is this repository; <version> is the workflow's tag and the CLI's version. Set the secrets with gh secret set <NAME>, then:

bash
gh workflow run deploy-switchboard.yml -f targets=all   # the first time
gh workflow run deploy-switchboard.yml                  # afterwards: only what is stale

Run the bot yourself ​

Without the Workers every optional capability is off. On any machine with Node and the installation init wrote:

bash
npx --yes @coreplane/switchboard@<version> start

The same bot as a container:

bash
docker run -d --name switchboard --restart unless-stopped --env-file .env -v "$PWD/config:/app/config:ro" ghcr.io/coreplanelabs/switchboard:latest

Next ​