# bohr > bohr deploys a GitHub repository as a Cloudflare Worker and lets an AI agent change it by conversation (in the web panel or on Telegram). Every change goes to a preview first, and reaches production only when someone approves it. bohr is open source and self-hosted, or run for you as bohr.io Cloud (app.bohr.io). # bohr documentation > bohr deploys your GitHub repository to Cloudflare and lets an AI agent change it by conversation, with a preview you approve before anything reaches production. Source: https://docs.bohr.io/ **Ask more, code less.** bohr turns a GitHub repository into a site that runs on Cloudflare and that you change by talking to an AI agent, in the web panel or on Telegram. Every change lands on a preview first. Nothing reaches production until someone approves it. ## Start here - [How it works](https://docs.bohr.io/introduction/how-it-works/): the path from a repository to production, step by step. - [bohr.io Cloud or self-hosted](https://docs.bohr.io/introduction/cloud-and-self-hosted/): the two ways to run bohr, and what each costs. - [Getting started](https://docs.bohr.io/getting-started/sign-in/): sign in, connect Cloudflare and GitHub, and create your first project. ## Use bohr - [The agent](https://docs.bohr.io/agent/chat/): ask for changes, review the preview, approve or discard, roll back. - [Projects](https://docs.bohr.io/projects/overview/): builds, versions, members, variables, password protection and domains. - [Deploys](https://docs.bohr.io/deploys/how-deploys-work/): how a project builds and publishes, and the files that describe it. - [Organizations](https://docs.bohr.io/organizations/overview/): work as a team, with members that mirror your GitHub organization. ## Automate - [The `bohr` CLI](https://docs.bohr.io/cli/install/): deploy from your terminal, scripts and CI. - [For AI agents](https://docs.bohr.io/ai-agents/overview/): let Claude Code, Cursor, Codex and other coding agents deploy with the CLI. - [GitHub Action](https://docs.bohr.io/github-action/reference/): `bohr-io/deploy-action@v1`. ## Run your own - [Self-hosting](https://docs.bohr.io/self-hosting/overview/): install the open source instance on your own Cloudflare account. - [Migrating from bohr v1](https://docs.bohr.io/migrating-from-v1/overview/): what happened to v1 projects and how to move them to your own account. :::tip[For AI tools] Every page has a Markdown copy: add `.md` to its address (for example [`/cli/deploy.md`](https://docs.bohr.io/cli/deploy.md)). The index of all pages is at [`/llms.txt`](https://docs.bohr.io/llms.txt), and the whole documentation in one file at [`/llms-full.txt`](https://docs.bohr.io/llms-full.txt). ::: --- # How it works > From a GitHub repository to production, through the agent, the preview and an approval. Source: https://docs.bohr.io/introduction/how-it-works/ bohr connects three things you already own: a **GitHub repository** (the code), a **Cloudflare account** (where the site runs) and a **model provider** (the AI agent's brain, such as an Anthropic API key or your Claude subscription). ``` You (web chat or Telegram) │ "Add a contact form to the home page" ▼ Project Worker in your Cloudflare account ├─ request queue, members, chat history ├─ agent sandbox (a Cloudflare Container): edits the code, runs the build └─ preview: the site, running from the "homolog" branch │ ▼ Approve Pull request homolog → production branch, merged │ ▼ GitHub Actions builds and deploys production to your Cloudflare account ``` ## The cycle 1. **Connect.** You sign in, connect your Cloudflare account and install bohr's GitHub App on the repositories bohr may use. You do this once. 2. **Create a project.** Pick one of your repositories, or start from a [template](https://docs.bohr.io/getting-started/templates/). bohr creates the project's Worker, the agent's sandbox and the preview in your Cloudflare account (one to three minutes). 3. **The agent prepares the repository.** On its first run, the agent works out how your project runs on Cloudflare: it writes `.bohr/preview.json` (how to run the preview) and `.bohr/deploy.json` (how to build and deploy production), adds a wrangler config if missing and makes the production build pass. 4. **Ask for a change.** Write what you want in plain words, in the panel's chat or to the project's Telegram bot. The agent edits the code in its sandbox and pushes the result to the `homolog` branch. 5. **Review the preview.** The preview (in Portuguese, *homologação*) shows the site with the change, on its own address. Ask for adjustments as many times as you want; follow-up requests continue the same conversation. 6. **Approve or discard.** **Approve and publish** opens a pull request from `homolog` to the production branch and merges it. GitHub Actions then builds and deploys production. **Discard** throws the changes away and the preview matches production again. 7. **Roll back** at any time: bohr merges a revert of the last release and production goes back to the previous version. ## What runs where | Piece | Where it runs | | --- | --- | | The bohr instance (panel, API, credential vault) | bohr.io Cloud, or your own Cloudflare account if you [self-host](https://docs.bohr.io/self-hosting/overview/) | | Each project's Worker, agent sandbox and preview | The project owner's Cloudflare account | | The production site | The project owner's Cloudflare account, as a Worker with static assets | | The code, branches, pull requests and deploy runs | The project's GitHub repository and GitHub Actions | | The AI model | Your provider: Anthropic, an Anthropic-compatible endpoint, or your Claude subscription | Projects migrated from bohr v1 are the exception: bohr hosts them in its own Cloudflare account until their owners [move them](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/). ## Safety by design - **Nothing reaches production without approval.** The agent can only push to `homolog`. Production changes only through an approved, merged pull request, and the merge names the exact commit you reviewed. - **The sandbox never holds your keys.** The agent sees placeholders; bohr adds the real model key and a short-lived, single-repository GitHub token on the way out. See [Security model](https://docs.bohr.io/reference/security/). - **The agent cannot change your workflows.** GitHub refuses any change under `.github/workflows` from the sandbox; bohr writes its own deploy workflow itself. --- # bohr.io Cloud or self-hosted > The two ways to run bohr, what each one includes, and what you pay for. Source: https://docs.bohr.io/introduction/cloud-and-self-hosted/ bohr is open source (MIT). You can run the **instance** (the panel, the API and the credential vault) yourself, or use the one bohr runs for you at [app.bohr.io](https://app.bohr.io). Either way, your projects run in your own Cloudflare account. | | bohr.io Cloud | Self-hosted | | --- | --- | --- | | Who runs the instance | bohr | You, on your Cloudflare account | | Address | `https://app.bohr.io` | Your Worker's address or your domain | | Price of the instance | US$20/month per account or organization | Free | | Where projects run | Your Cloudflare account | Each user's Cloudflare account | | Features | All | All | | Users and projects | Per subscription | Unlimited | | Telemetry | None sent | Off unless an admin turns it on ([details](https://docs.bohr.io/self-hosting/telemetry/)) | ## What you pay for On both, infrastructure is paid directly to the providers, never through bohr: | Provider | What it charges for | | --- | --- | | Cloudflare | The **Workers Paid** plan (US$5/month), required because the agent's sandbox is a Container. Container time while the agent works (a `standard-1` container costs cents per hour and sleeps when idle), and whatever your sites use in production. | | Your model provider | The agent's tokens, with your Anthropic API key or a compatible provider. With a Claude Pro or Max subscription, usage counts against your subscription instead. | | GitHub | Nothing extra: Actions minutes for the deploy runs, within your plan. | Each request has a spending cap (US$5 by default), which you can change per project. See [Costs and limits](https://docs.bohr.io/agent/costs-and-limits/). ## bohr.io Cloud - Nothing to install: sign in at [app.bohr.io](https://app.bohr.io), subscribe and connect your accounts. - One subscription per personal account, and one per [organization](https://docs.bohr.io/organizations/overview/), paid by one of its admins. Members of an organization do not need a subscription of their own to work on its projects. - Billing goes through Stripe. See [Subscription](https://docs.bohr.io/billing/subscription/). ## Self-hosted - Deploy the instance with the **Deploy to Cloudflare** button or with `wrangler`, then create its GitHub App at `/setup`. See [Self-hosting](https://docs.bohr.io/self-hosting/overview/). - The instance itself fits in Cloudflare's free plan to start; each user still needs Workers Paid on the account where their projects run. - You keep the master key of the credential vault, and you decide who can sign in. :::note Whoever runs an instance keeps its users' Cloudflare tokens (encrypted) and its GitHub App key. Use an instance you trust: bohr.io Cloud, or your own. ::: --- # What you need > The accounts and plans each person needs to use bohr. Source: https://docs.bohr.io/introduction/requirements/ ## To use bohr | You need | Why | | --- | --- | | A **GitHub** account | Your code lives in your repositories. bohr reads and changes them through its GitHub App, only on the repositories you choose. | | A **Cloudflare** account on **Workers Paid** | Each project's Worker, the agent's sandbox (a Container) and the preview run there, and so does your production site. The account also needs a `workers.dev` subdomain: open **Workers & Pages** once in the Cloudflare dashboard to create it. | | A **model provider** for the agent | An Anthropic API key, an endpoint compatible with the Anthropic Messages API (OpenRouter, DeepSeek, Kimi, GLM, a LiteLLM proxy…), or a Claude Pro/Max subscription. Without one, projects still deploy what reaches GitHub, but the agent is off. | | On bohr.io Cloud: a **subscription** | US$20/month per personal account or organization. Not needed on a self-hosted instance. | | Optional: **Telegram** | To talk to the agent and approve releases from your phone. The web chat does everything without it. | ## To work on someone else's project Nothing but a bohr account. The project owner [invites you](https://docs.bohr.io/projects/members/) with a code, or adds you through their [organization](https://docs.bohr.io/organizations/members/). Members use the owner's Cloudflare account and agent; they need no subscription, Cloudflare account or model key of their own. ## Supported projects The agent maps your repository to Cloudflare on its first run: | In the repository | Production | Preview | | --- | --- | --- | | JavaScript/TypeScript or Python without a Dockerfile | A Worker with static assets, using the framework's official Cloudflare adapter (OpenNext for Next.js 15.5+, the Nuxt `cloudflare` preset, `@astrojs/cloudflare`, `@sveltejs/adapter-cloudflare`, React Router or Remix for Cloudflare). A plain static site is assets only. | The framework's dev server | | A Dockerfile | A Container behind a Worker | The dev server | | Postgres | Neon in your account, through Hyperdrive | A Neon branch | | Redis | Upstash in your account | `redis-server` in the sandbox | | Files written to local disk | R2 | An R2 bucket for the preview | Stateful services such as volumes, MongoDB or Elasticsearch are not supported; the agent says so and suggests the closest alternative. When a change moves your queues or disk writes to Cloudflare services, the agent asks before doing it. --- # Sign in > Sign in to bohr with GitHub or with a code sent by email, then follow the Get started wizard. Source: https://docs.bohr.io/getting-started/sign-in/ Open the instance: [app.bohr.io](https://app.bohr.io) for bohr.io Cloud, or your own instance's address. ## With GitHub Click **Sign in with GitHub** and authorize bohr. Your name and email come from GitHub. ## With a code by email 1. Type your email and click **Email me a code**. 2. bohr sends a **6-digit code**, valid for 10 minutes. 3. Type the code and click **Sign in**. A code allows 5 attempts. If you ask for too many codes, bohr answers *"Too many attempts. Wait a few minutes and try again."* :::note On a self-hosted instance, email sign-in only appears when its administrator set it up ([how](https://docs.bohr.io/self-hosting/optional-features/#sign-in-with-a-code-by-email)). The first person who signs in to a new instance becomes its administrator. ::: Your session lasts 30 days on that browser. **Account › Sign out** ends it. You can add or remove ways to sign in later in [Connected accounts](https://docs.bohr.io/account/connections/). ## The Get started wizard After the first sign-in, the panel opens **Get started**. It asks only for what every user sets up once: | Step | What you do | | --- | --- | | **Subscription** | bohr.io Cloud only: click **Subscribe**, pay on Stripe and come back. See [Subscription](https://docs.bohr.io/billing/subscription/). | | **Cloudflare** | Create an API token and paste it. See [Connect Cloudflare](https://docs.bohr.io/getting-started/connect-cloudflare/). | | **GitHub** | Install bohr's GitHub App on your repositories. See [Install the GitHub App](https://docs.bohr.io/getting-started/install-github-app/). | When it shows **All set!**, click **New project**, or **Set up the agent** first if you have not chosen a model provider yet ([Model providers](https://docs.bohr.io/agent/providers/)). :::tip Working only on projects of an [organization](https://docs.bohr.io/organizations/overview/) you belong to? You do not need this setup: the organization's admins already connected its subscription, Cloudflare account and agent. Switch to the organization in the context menu at the top of the panel. ::: --- # Connect Cloudflare > Create the Cloudflare API token bohr needs, with the right permissions, and connect your account. Source: https://docs.bohr.io/getting-started/connect-cloudflare/ bohr creates each project's Worker, the agent's sandbox and the preview in **your** Cloudflare account, with an API token you give it. The instance keeps the token encrypted and only uses it to create and update your projects' Workers. ## Before you start - The account must be on the **Workers Paid** plan: the agent's sandbox is a Cloudflare Container. - The account needs a **workers.dev subdomain**. If you never opened **Workers & Pages** in the Cloudflare dashboard, open it once: that creates the subdomain. ## Create the token 1. In the **Cloudflare** step, click **Create the token on Cloudflare**. It opens Cloudflare's token page with the main permissions filled in. 2. **Before saving, add the `Containers: Edit` permission.** The link cannot fill it in. 3. Create the token, copy it, paste it in **Cloudflare API token** and click **Connect**. The token needs these permissions: | Permission | Why | | --- | --- | | **Workers Scripts: Edit** | Creates the project Workers. It also covers Durable Objects and the workers.dev subdomain. | | **Account Settings: Read** | Reads the account's details. | | **Containers: Edit** | The agent's sandbox. Needs the Workers Paid plan. **Add it by hand.** | | **D1: Edit** | Creates the database of projects started from a [template](https://docs.bohr.io/getting-started/templates/). Filled in by the link; only template projects need it. | When something is missing, the panel shows *"The token lacks some permissions"* with the list. Edit the token on Cloudflare (or create another one from the template), add what is missing and paste it again. ### Several accounts If the token can see more than one Cloudflare account, bohr asks **Choose the Cloudflare account**: pick the one your projects should run in and click **Use this account**. ## What bohr leaves in your account So you can tell bohr's work apart from yours: - Every Cloudflare API call bohr makes sends `User-Agent: bohr/ (+https://bohr.io)`; it shows in your account's audit log. - The Workers bohr creates (each project's agent Worker and preview Worker) carry the tags `bohr` and `bohr-project:`, and their deployments the message `bohr`. - Production deploys run `wrangler deploy --message "bohr" --tag "bohr-"`. ## Later - Connected accounts are listed in **Account › Connected accounts › Cloudflare**. You can [disconnect](https://docs.bohr.io/account/connections/) an account once no project uses it; also delete the token in the Cloudflare dashboard (**My Profile → API Tokens**). - An [organization](https://docs.bohr.io/organizations/settings-and-billing/) connects its own Cloudflare account, separate from yours. --- # Install the GitHub App > Give bohr access to the repositories it may use, through its GitHub App. Source: https://docs.bohr.io/getting-started/install-github-app/ bohr reads and changes your repositories through its **GitHub App**, only on the repositories you select. 1. In the **GitHub** step, click **Install on GitHub**. The installation opens in a new tab. 2. Pick your account (or an organization you own) and choose **Only select repositories** or **All repositories**. 3. Confirm, come back to the panel and click **I installed it**. ## What the App can do GitHub shows these permissions before you confirm: | Permission | Why | | --- | --- | | Contents: write | The agent pushes to the `homolog` branch; approvals merge to production | | Pull requests: write | Approvals open and merge pull requests; rollbacks open revert pull requests | | Workflows: write | bohr writes the deploy workflow. The agent's sandbox never gets this permission. | | Secrets: write | Removes Actions secrets that older versions of bohr wrote | | Actions: read | bohr tells you when the production deploy finishes, and shows the [Builds](https://docs.bohr.io/projects/builds/) | | Metadata: read | Lists your repositories | | Organization › Members: read | [Organizations](https://docs.bohr.io/organizations/overview/) mirror the members of the GitHub organization | | Administration: write | Creates repositories from [templates](https://docs.bohr.io/getting-started/templates/), only with your own authorization | The tokens bohr creates from the App are short-lived and limited to one repository. The token the agent's sandbox uses can only write contents. ## Give access to more repositories When creating a project, a repository missing from the list usually means the App cannot see it. Click **Give access to more repositories on GitHub**, add the repository on GitHub, then **Refresh the list**. Repositories that are already bohr projects do not show up in the list. ## Remove access **Account › Connected accounts › GitHub App** lists the accounts where the App is installed. **Disconnect** uninstalls it from that account; projects that still use it must be deleted first. Your repositories are not changed. --- # Create a project from a repository > Turn one of your GitHub repositories into a bohr project. Source: https://docs.bohr.io/getting-started/create-project/ A **project** is one repository, one production site and one agent. To create one, click **New project** in the sidebar. ## 1. Choose the repository 1. Under **How to start**, keep **One of my repositories**. 2. Search the list by name or organization and pick the repository. Private repositories show a **Private** badge. 3. Check the two fields: - **Project name**: how the project appears in bohr. - **Production branch**: approved changes are merged into this branch, and the production deploy starts from it. It defaults to the repository's default branch. 4. Click **Create the project**. The repository you want is missing? See [Give access to more repositories](https://docs.bohr.io/getting-started/install-github-app/#give-access-to-more-repositories). :::note[bohr.io Cloud] Creating projects needs an active subscription. Without one, the panel shows *"Creating projects on bohr.io Cloud needs an active subscription."* with a link to subscribe. ::: ## 2. Setup bohr creates, in your Cloudflare account: - the **project Worker**, with the request queue, the members and the chat; - the **agent's sandbox**, a Container that sleeps when idle; - the **preview Worker**, an unlisted `workers.dev` address that shows the `homolog` branch. The panel shows the progress (*Creating the project Worker*, *Preparing the agent's sandbox*, *Turning the preview on*, *Almost there*). It usually takes one to three minutes; keep the page open. If it fails, the panel shows *"The project setup failed"* with the reason and a way to try again. The most common causes are in [Troubleshooting](https://docs.bohr.io/reference/troubleshooting/#project-setup). ## 3. Telegram bot (optional) bohr offers to create a **project bot** on Telegram, so you and your team can ask for changes and approve releases from Telegram. Follow the steps (BotFather, `/newbot`, paste the token) or click **Skip and open the project**. You can connect a bot later in **Settings › Agent**. See [Telegram](https://docs.bohr.io/agent/telegram/). ## 4. The agent prepares the repository As soon as the project is ready and your [model provider](https://docs.bohr.io/agent/providers/) is set up, the agent starts **preparing the repository** on its own. Continue in [First deploy](https://docs.bohr.io/getting-started/first-deploy/). ## Projects in an organization To create the project in an organization, switch to it first (context menu at the top of the panel) and click **New project** there. The project runs in the organization's Cloudflare account and uses its agent settings, whoever creates it. See [Organizations](https://docs.bohr.io/organizations/overview/). --- # Start from a template > Create a project from one of bohr's ready-made sites, with its own repository and D1 database. Source: https://docs.bohr.io/getting-started/templates/ A template is a small, useful site you can publish right away and then change by asking the agent. bohr creates a **GitHub repository** from the template, a **D1 database** in your Cloudflare account, and publishes the site. ## Create it 1. Click **New project** and, under **How to start**, choose **A template**. 2. Pick a template. 3. Fill in: - **Project name**: also the name of the GitHub repository (letters, numbers, `.`, `-` and `_`). - **Owner**: where the repository is created on GitHub, **My personal account** or one of your organizations. The project runs in that owner's Cloudflare account. - **Private repository**: only people with access to the repository see the code. The published site is open either way. 4. Click **Create the project**. GitHub asks you to authorize bohr to create the repository on your behalf. Your Cloudflare token needs the **D1: Edit** permission. Without it, the panel says *"The Cloudflare token cannot create D1 databases."* and links to **Edit the token on Cloudflare**. ## The admin password Every template has an admin area at `/admin`. bohr generates its password and shows it **once**, under **Admin area password**: save it then. It is the `ADMIN_PASSWORD` [site variable](https://docs.bohr.io/projects/variables/), where you can change it later. The site is online as soon as the first deploy finishes (a few minutes). ## The templates Every template is one Cloudflare Worker ([Hono](https://hono.dev) API plus static files) with a D1 database. Its pages speak Brazilian Portuguese, English and Spanish, following the visitor's browser. | Template | What you get | | --- | --- | | **Waitlist** | A launch page that collects names and emails of interested people, without duplicates, with an admin to see, search and export sign-ups as CSV. Each visitor sees their position in line. | | **Booking** | An online booking page where customers pick a service, a day and a free time, with an admin to confirm bookings and manage services and opening hours. Bookings never overlap. | | **Link in bio** | A page with your photo, bio and all your links, with click counts per link and an admin to edit, hide and reorder them. | | **Forms and surveys** | Build forms with text, email, multiple choice and star rating questions, share the link and see the answers as charts or export them as CSV. | | **Digital menu** | An online menu with a cart where customers build their order and send it on WhatsApp, with an admin to manage orders, items and prices. | | **Short links** | A URL shortener on your own domain, with custom links, clicks per day and an admin to create, edit and turn links off. | The templates' source is public in the `bohr-io/template-` repositories on GitHub (for example `bohr-io/template-waitlist`). ## How a template project is set up - `wrangler.jsonc` gets the Worker's name (the project name) and the new database. - `.bohr/deploy.json` and `.bohr/preview.json` tell bohr how to deploy and preview it. - Database schema changes are new files in `migrations/`; each production deploy applies them first (see [D1 databases](https://docs.bohr.io/deploys/d1-databases/)). From there it is a project like any other: ask the agent for changes. Each template's README lists ideas, such as *"Send a welcome email to each new sign-up"* for the waitlist. If something fails after GitHub created the repository, bohr undoes what it created (database, project) except the repository, and the error message names it. --- # First deploy > How the agent prepares your repository and how your site reaches production the first time. Source: https://docs.bohr.io/getting-started/first-deploy/ A new project from one of your repositories does not deploy anything until the repository is **prepared** for bohr. The agent does that in its first run. ## Preparing the repository When the project is ready and your [model provider](https://docs.bohr.io/agent/providers/) is set up, the agent starts on its own. In the chat you see the request *"Prepare this repository for bohr…"* and its progress. The agent: 1. Inspects the repository and decides how it runs on Cloudflare: static assets, a Worker, or a Container behind a Worker. 2. Writes [`.bohr/preview.json`](https://docs.bohr.io/deploys/preview-json/), so the preview can run the project's dev server. 3. Adds a `wrangler.jsonc` if the repository has none. 4. Writes [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/), from which bohr generates the production workflow, `.github/workflows/bohr-deploy.yml`. 5. Makes the production build pass. 6. Removes workflows of bohr v1 (`bohr-io/action`), which fail on every push. 7. Tells you what it found, what changed, and anything that cannot be supported. It takes a few minutes. The project's **Overview** shows *Preparing the repository* in **What is missing** meanwhile. No provider yet? The project waits: as soon as you save the agent settings, the agent prepares every project that was waiting. You can also start it by hand with **Prepare the repository** in the chat, or **Prepare repository** in the checklist. ## Publishing for the first time The preparation ends like any other request: with a **preview** and the buttons **Approve and publish** and **Discard**. 1. Open the preview and check the site. 2. Click **Approve and publish**. bohr opens a pull request from `homolog` to your production branch and merges it. 3. The merge runs `bohr-deploy.yml` in GitHub Actions. It asks bohr for your Cloudflare credentials at run time, builds the site and runs `wrangler deploy` to your account. 4. The chat says *Production updated* when the run finishes. Follow it in [Builds](https://docs.bohr.io/projects/builds/). Your site is now a Worker in your Cloudflare account, at its `workers.dev` address. Add your own domain in [Domains](https://docs.bohr.io/projects/domains/). :::tip Template projects skip this step: their repository already has everything, so their first deploy starts as soon as the project is created. ::: ## When the agent is off Without a model provider, a project only publishes what reaches GitHub: a push to the production branch runs the deploy workflow, if the repository has one. The project shows **Deploys only** in the list. --- # Your first request > Ask the agent for a change, review it on the preview and publish it. Source: https://docs.bohr.io/getting-started/first-request/ With the repository prepared, ask for something small to see the whole cycle. 1. Open the project and go to **Agent** (or open the site view on the **Overview** and click **Chat**). 2. In **Ask for a change…**, write what you want, for example: > Change the title on the home page to "Fresh bread every morning" and make the main button orange. 3. Press Enter to send (Shift + Enter adds a line break). 4. The chat shows **The agent is working** with short status lines (*Editing src/pages/index.astro*, *Running npm run build*…). You can **Cancel the request** while it runs. 5. When it finishes, the agent writes a summary of what changed and what to check, and a card **Preview ready · version 1** with: - **See the preview**: the site with the change, on the preview address; - **See the changes on GitHub**: the exact diff you are approving; - **Approve and publish** and **Discard**. 6. Not quite right? Just send another message, such as *"Make the button a bit bigger"*. The agent continues the same conversation and the preview gets a new version. 7. Click **Approve and publish**. bohr merges the change into production and GitHub Actions deploys it. The chat tells you when production is updated. That's it. Next: - [Tips for good requests](https://docs.bohr.io/agent/tips/) - [The preview, approving and discarding](https://docs.bohr.io/agent/previews/) - [Ask from Telegram](https://docs.bohr.io/agent/telegram/) - [Invite your team](https://docs.bohr.io/projects/members/) --- # Chat in the panel > Talk to the agent in the project's Agent page or beside the site view, follow its work and answer its questions. Source: https://docs.bohr.io/agent/chat/ Every project has one conversation with the agent, shared by all its members, on the web and on [Telegram](https://docs.bohr.io/agent/telegram/). ## Where to chat - **Agent** (project menu): the full conversation, with the preview decisions. - **Overview › site view**: open the site in full screen and click **Chat** to talk to the agent beside it. **Close the chat** hides it again. ## Sending a request Type in **Ask for a change…** and press Enter (Shift + Enter for a new line). A request can be up to 20,000 characters. - Requests run **one at a time**, in order. While one runs, new ones wait; the chat shows **Requests in line: N**. - Each member can have up to **5 requests waiting**. - **Cancel the request** stops the running one (or removes it from the line). Members cancel their own requests; the owner can cancel anyone's. ## While the agent works **The agent is working** shows a short status line that changes as it goes: *Editing src/App.vue*, *Running npm run build*, *Searching…*. The agent may also write messages to you during the run. A request can run for at most 2 hours, within its [budget](https://docs.bohr.io/agent/costs-and-limits/). ## When it finishes | Outcome | What you see | | --- | --- | | Changes made | A summary for a non-technical reader (a title and a few bullet points) and the card **Preview ready · version N** with **See the preview**, **See the changes on GitHub**, **Approve and publish** and **Discard**. | | Nothing to change | The summary and *Nothing changed in the preview.* | | A question | The agent needs a decision only you can make. It shows the question with options as buttons; tap one or type your own answer. The answer continues the same work. | | Failed or cancelled | Why it stopped. The preview was not changed. | Follow-up requests within the same preview continue the same conversation, so *"make it bigger"* has context. After you approve or discard, the next request starts fresh. ## When the agent is off *"The agent is off because it has no provider yet."* The project owner (or an organization admin) must choose a [model provider](https://docs.bohr.io/agent/providers/). Until then the project still publishes what reaches GitHub. ## Language The agent writes to you in the project's **Agent language** (Brazilian Portuguese, English or Spanish), set in **Settings › Agent**. On Telegram, the bot follows each person's Telegram language. --- # Telegram > Talk to the agent and approve releases from the project's Telegram bot, and the commands it understands. Source: https://docs.bohr.io/agent/telegram/ Each project can have its own **Telegram bot**. It is the same conversation as the web chat: you ask for changes, open the preview and approve or discard with buttons. ## Connect a bot 1. On Telegram, open **@BotFather** and send `/newbot`. 2. Choose a name and a username that ends in `bot`. 3. Copy the token BotFather sends (it looks like `123456789:AA…`). 4. In the project, open **Settings › Agent**, paste it under **Telegram** and click **Connect the bot**. (The new-project flow offers the same step.) Only the project owner connects the bot. To stop using it, delete it with BotFather; bohr does not delete project bots. ## Link your Telegram account The bot recognizes people by their bohr account. In **Account**, click **Link Telegram**, then **Open in Telegram** and tap **Start**. The link works for a few minutes. (This needs the instance's platform bot, which bohr.io Cloud has; on a self-hosted instance the administrator sets it up.) Someone who is not a member yet gets: *"You don't have access to … yet. Ask the owner for an invite and send /join <code>."* ## Talking to the bot - **In a private chat**, every message is a request. - **In a group**, the bot answers messages that mention it (`@your_bot`), replies to its own messages, and commands. While the agent works, the bot keeps one status message up to date. When it finishes, it sends the summary and the buttons **Approve and publish**, **Discard**, **🔎 See the preview** and **See the changes on GitHub**, bound to that preview version: a button pressed on an old version answers *"The preview changed since this message."* ## Commands Each command also has a Portuguese and a Spanish name. | Command | Also | What it does | | --- | --- | --- | | `/status` | `/estado` | The running request, the line and the preview | | `/approve`, `/discard` | `/aprovar`, `/aprobar`, `/descartar` | Shows the current preview's changes with the buttons to decide | | `/cancel` | `/cancelar` | Cancels your running or waiting request | | `/rollback` | `/reverter`, `/revertir` | Returns production to the previous version | | `/invite` | `/convidar`, `/invitar` | Creates an invite code (owner) | | `/join ` | `/entrar`, `/unirse` | Joins the project with an invite code | | `/members` | `/membros`, `/miembros` | Lists the members with numbers | | `/revoke ` | `/revogar`, `/revocar` | Removes a member (owner) | | `/model ` | `/modelo` | Sets the project's model (owner) | | `/budget ` | `/orcamento`, `/presupuesto` | Sets the budget per request, from 0.10 to 1000 (owner) | | `/help` | `/ajuda`, `/ayuda` | Lists the commands | Typed `/approve` and `/discard` never act by themselves: they show the buttons for the version you are looking at, so you always decide on what you reviewed. ## The platform bot Instances with a platform bot (such as bohr.io Cloud's) also let you go through onboarding on Telegram: `/new` creates a project, `/status` lists your projects and `/retry` repeats the last step that failed. The agent settings are still chosen in the web panel. --- # The preview (homologação) > Review the agent's changes on the preview, then approve them to production or discard them. Source: https://docs.bohr.io/agent/previews/ The **preview** (in the Portuguese panel, *homologação*) is your site running from the `homolog` branch, where the agent commits its work. It collects the changes of every finished request until someone approves or discards them. ## Its state The project shows the preview in one of three states: | State | Meaning | | --- | --- | | **Same as production** | `homolog` matches production. Nothing to review. | | **Changes to review** | The agent pushed changes nobody decided on yet. Each push is a new **version** (1, 2, 3…). | | **Publishing** | Someone approved; bohr is merging and GitHub Actions is deploying. | ## Reviewing Open **See the preview** on the Overview or in the chat, or choose **Preview** in the selector of the site's full-screen view (opening the **Chat** there switches to it): - Beside the selector, *Same as production* or *2 changes* (hover it for what changed). - Switch between **Desktop**, **Tablet** and **Phone**, or open it in a new tab. - The Overview's preview card lists what the agent did, with **Approve and publish** and **Discard**. - **See the changes on GitHub**, in the chat, opens the diff of exactly the commit that approving publishes. The chat never prints the preview's address: on Telegram, the **🔎 See the preview** button opens it. The preview address is public but unlisted: an unguessable `workers.dev` name, marked `noindex` for search engines. Share it with whoever needs to review. The preview is the project's **dev server** running in the agent's sandbox, as configured in [`.bohr/preview.json`](https://docs.bohr.io/deploys/preview-json/). The sandbox sleeps when idle: the first visit after a while shows *"The preview is starting. This page reloads by itself."* while it installs and starts again. ## Approve and publish Any member can approve. **Approve and publish**: 1. opens (or reuses) a pull request from `homolog` to the production branch; 2. squash-merges it **only if its head is still the version you reviewed**, so changes pushed after your review cannot slip in; 3. resets `homolog` to the merge, and the next request starts a fresh conversation; 4. GitHub Actions deploys production. The chat says *Production updated* when the run finishes, or *The production deploy failed* with a link to the run. You cannot decide while a request is running (*"Wait for the running request to finish before deciding."*). A request that finishes while an approval is publishing waits for the next approval. ## Discard **Discard** (confirm with *Discard the changes?*) resets `homolog` to production. The changes that were not published are lost, and the next request starts a fresh conversation. ## Approving from Telegram The project bot sends the same buttons, bound to the version they were sent with. See [Telegram](https://docs.bohr.io/agent/telegram/). --- # Versions and rollback > Every release bohr published, and how to return production to the previous version. Source: https://docs.bohr.io/agent/versions-and-rollback/ Each approval becomes a pull request merged into the production branch, and GitHub Actions publishes it. **Versions** (project menu) lists these releases, newest first. ## The list Each release shows when it was **Published**, the pull request it came **via** (**PR #N**, or a **direct commit**), links to **View PR** and **View commit**, and its status: | Status | Meaning | | --- | --- | | **Publishing** | The GitHub Actions run is deploying it | | **Live** | The run finished; production serves it | | **Failed** | The run failed; production kept the previous version | | **Rolled back** | A rollback reverted it | ## Roll back Click **Roll back the last release** and confirm. bohr opens and merges a pull request that **reverts** the last release's pull request, using GitHub's revert. GitHub Actions then publishes the previous version. On Telegram, send `/rollback`. - Rolling back is itself a release: it shows in the list, and it can be rolled back too. - It reverts code. Data your site wrote (for example rows in a D1 database) and schema migrations that already ran are not undone. - If GitHub cannot revert cleanly (a later change touched the same lines), bohr says *"I couldn't roll back"* with the reason. Ask the agent to undo the change instead. ## Changes made outside bohr Pushes to the production branch that did not come from an approval (yours, your team's, or another tool's) also deploy, through the same workflow. They show in [Builds](https://docs.bohr.io/projects/builds/). --- # Model providers > Power the agent with an Anthropic API key, an Anthropic-compatible endpoint, or your Claude Pro/Max subscription. Source: https://docs.bohr.io/agent/providers/ The agent needs a model. You choose it once in **Account › Agent** (**Set up the agent**), and it applies to every project you own. An [organization](https://docs.bohr.io/organizations/settings-and-billing/) chooses its own for its projects. The key is stored encrypted and reaches each project's Worker as a secret. The agent's sandbox never sees it: it holds a placeholder, and bohr adds the real key on the way out. ## Anthropic API key (recommended) 1. Create a key in the [Anthropic Console](https://console.anthropic.com). It starts with `sk-ant-`. 2. Under **Provider**, choose **Anthropic API key** and paste it in **Anthropic API key**. 3. **Model**: leave blank to use bohr's default, `claude-opus-5-5`, or type another Claude model id. 4. **Effort**: how much the model thinks before acting. **Model default**, **Low**, **Medium**, **High**, **Very high** or **Maximum**. More effort means more careful work and more tokens. 5. Click **Save**. bohr checks the key with Anthropic before saving. ## Compatible provider Any API compatible with Anthropic's Messages API: OpenRouter, DeepSeek, Kimi, GLM, a LiteLLM proxy in front of other providers… | Field | Value | | --- | --- | | **Base URL** | The API address before `/v1/messages`, with `https`. For example `https://openrouter.ai/api` or `https://api.deepseek.com/anthropic`. | | **Provider's API key** | The key your provider gave you. bohr checks that the provider accepts it. | | **Model** | Required: the model's name at the provider, such as `deepseek-chat` or `moonshotai/kimi-k2`. | | **Effort** | Optional. Compatible providers may ignore it. | How well the agent works depends on the model: it edits files, runs commands and follows long instructions. ## Claude subscription (Pro/Max) Use your Claude Pro or Max plan instead of an API key. The agent runs through the official Claude Code, and usage is subject to your subscription's terms and limits. 1. Choose **Claude subscription (Pro/Max)** and click **Connect Claude subscription**. 2. Click **Authorize on claude.ai** and authorize with your Claude account. 3. Copy the code claude.ai shows, paste it in **Authorization code** and click **Finish connecting**. Behind the scenes, bohr runs the official CLI's `claude setup-token` in an isolated container and keeps the long-lived token it creates (it starts with `sk-ant-oat01-`). **I already have a token**: in a terminal with Claude Code installed, run: ```sh claude setup-token ``` sign in with your account, and paste the token it shows in **Claude Code token**. :::note On some self-hosted instances the connection runs in the container of one of your projects. If none is ready yet, the panel says so: create a project first, or paste a token from `claude setup-token`. ::: With a subscription, nothing is charged per request. The [budget per request](https://docs.bohr.io/agent/costs-and-limits/) still applies, as a safety cap estimated at API prices. ## Changing or removing - Saving applies the new settings to every ready project at once (*Saved and applied to N projects.*). Projects that were waiting for an agent start [preparing their repository](https://docs.bohr.io/getting-started/first-deploy/). - To keep the stored key and change only the model or effort, leave the key field blank. - **Remove** (*Turn the agent off?*) deletes the key and turns the agent off in all your projects. GitHub deploys keep working. - A project can use a different **model** than your account's: see [Project settings](https://docs.bohr.io/projects/settings/#agent). --- # What the agent can and cannot do > The agent's tools, its sandbox, and the limits bohr puts around it. Source: https://docs.bohr.io/agent/capabilities/ The agent is built on the Claude Agent SDK and runs in an isolated **sandbox** (a Cloudflare Container in the project owner's account) with a fresh copy of your repository. ## What it can do - **Read and change any file** in the repository, and create new ones. - **Run commands**: install dependencies, build, run tests, type checks and linters. It is asked to run the project's checks before finishing and fix what its change broke. - **Search** the code, and look things up on the web when the model supports it. - **Run the preview**: keep `.bohr/preview.json` working so you can see the change. - **Set up production**: keep `.bohr/deploy.json` and the wrangler config working when a change affects the build. - **Ask you** when a decision belongs to you (a choice between alternatives it cannot infer), with options as buttons. - **Follow your repository's instructions**: it reads the repository's `CLAUDE.md` and `.claude/settings.json`. - **Keep context** across follow-up requests in the same preview. It writes a summary for a non-technical reader at the end of each request: a title (which becomes the commit message) and a few bullet points on what changed and what to check. ## What it cannot do | It cannot | Because | | --- | --- | | Publish to production | It can only push to `homolog`. Production changes only when someone approves. | | Push, pull, merge, rebase or switch branches | bohr commits everything left in the working tree and pushes it to `homolog` when the agent finishes. | | Change GitHub Actions workflows | GitHub refuses changes under `.github/workflows` from the sandbox's token, and bohr puts those files back before committing. If a workflow needs a change, the agent says so in its summary. | | Change git remotes, credentials or `.git` | Refused by bohr's guard rails. | | Write outside the repository and `/tmp` | Refused by bohr's guard rails. | | See your keys | The sandbox only holds placeholders; bohr adds the real model key and a short-lived GitHub token on the way out, only while a request runs. | | Keep files between sessions | The sandbox's disk is wiped when it sleeps; only what was pushed to GitHub remains. | | Run git hooks | Commits and pushes skip hooks; the agent runs the checks itself instead. | Files that are never committed, even when `.gitignore` misses them: `node_modules/`, `.venv/`, `__pycache__/` and `.wrangler/`. ## When a request fails A request stops when it reaches its **budget** or its **maximum turns**, on an error, or when someone cancels it. Then `homolog` goes back to where it was before the request: nothing half-done is pushed. The chat explains why, in your language. See [Costs and limits](https://docs.bohr.io/agent/costs-and-limits/). ## Stateful services and databases On its first run, the agent maps the project to Cloudflare (see [What you need](https://docs.bohr.io/introduction/requirements/#supported-projects)). When a project needs a database or Redis and no connection details are configured, the agent explains what you need to provide. Changes that move queues to Cloudflare Queues or disk writes to R2 change your code, so the agent asks before doing them. --- # Costs and limits > What each request costs, the budget and turn limits, and how to change them. Source: https://docs.bohr.io/agent/costs-and-limits/ ## Cost of each request The agent spends model tokens at your provider's prices, and you pay them to your provider, not to bohr; with a [Claude subscription](https://docs.bohr.io/agent/providers/#claude-subscription-promax) they are part of it. The chat never talks about cost: your provider's console shows what you spent. Container time is billed by Cloudflare to the account that runs the project: the sandbox runs while the agent works and while the preview is being visited, then sleeps. ## Limits per request | Limit | Default | Where to change it | | --- | --- | --- | | **Budget per request (US$)** | 5 | **Settings › Agent** (owner), or `/budget` on Telegram: 0.10 to 1000. | | **Maximum turns** | 200 | **Settings › Agent** (owner): 1 to 1000. A turn is one round of the agent's work. | | Running time | 2 hours | Fixed | | Request length | 20,000 characters | Fixed | | Waiting requests | 5 per member | Fixed | When a request reaches its budget, it stops and nothing is pushed: *"the request reached the per-request limit before the work was done. The owner can raise it in the agent's settings."* With a Claude subscription the budget is a safety cap estimated at API prices; nothing is charged separately. ## Keeping costs down - Ask for one thing at a time, and be specific (see [Tips](https://docs.bohr.io/agent/tips/)). - Lower the **Effort** in [Model providers](https://docs.bohr.io/agent/providers/) for simple projects. - Choose a smaller model for a project in **Settings › Agent › Model**. - Approve or discard when you are done: follow-ups in a long preview carry the whole conversation. --- # Tips for good requests > How to ask so the agent gets it right the first time. Source: https://docs.bohr.io/agent/tips/ The agent writes for people who do not code, and it works best with requests that say **what** you want and **where**, in plain words. ## Do - **Name the place.** *"On the pricing page, under the three plans…"* beats *"add a note somewhere"*. - **Describe the result, not the code.** *"Visitors should be able to send us a message with their name and email, and we get it by email"*. - **Give the exact text** when wording matters: *"Change the headline to: Fresh bread every morning"*. - **One change per request**, then adjust. Small requests are faster, cheaper and easier to review. - **Say what to keep.** *"Keep the current colors"*, *"Don't change the menu"*. - **Use follow-ups.** The agent remembers the conversation until you approve or discard: *"Now make it smaller on phones"*. - **Mention secrets by name, never their value.** *"Use the RESEND_API_KEY variable to send the email"*, then add the variable in [Environment variables](https://docs.bohr.io/projects/variables/). ## Avoid - Pasting passwords, API keys or tokens in the chat. The chat is stored and shared with all members. - Huge, vague requests (*"redo the whole site"*). Break them into steps. - Asking the agent to deploy or merge: approving does that. - Asking it to change GitHub workflows: it cannot (see [capabilities](https://docs.bohr.io/agent/capabilities/)). ## Examples > Add a FAQ section at the end of the home page with these three questions and answers: … > The contact form shows an error on phones. Fix it and check it works on a small screen. > Add a page /team with a photo, name and role for each person in this list: … > Create a waitlist form that saves name and email in the database, and an /admin page protected by the ADMIN_PASSWORD variable to see the sign-ups. ## Review well - Open the preview on **Phone** too. - Read the summary: it lists what to check. - When in doubt, **See the changes on GitHub**: it shows exactly what approving publishes. --- # The project page > The project list, the project's menu and its overview. Source: https://docs.bohr.io/projects/overview/ ## The project list **Projects** shows your projects (or the current organization's). Each card shows the repository, the production and preview addresses, the Telegram bot if there is one, and a state: | State | Meaning | | --- | --- | | **Agent ready** | The project's Worker is running and the agent can work | | **Setting up** | bohr is creating the project's Worker, sandbox and preview | | **Setup failed** | Open the project to see why and try again | | **Deploys only** | No agent: the project publishes what reaches GitHub | | **Migrating** | A project from bohr v1 being moved | | **Suspended** | The project does not deploy; contact the instance's administrator | Projects where you are a member but not the owner show a **Member** badge. ## The project menu | Page | What it is for | | --- | --- | | **Overview** | Addresses, the agent's activity, the preview, the latest version, and what is missing | | **Agent** | The [chat](https://docs.bohr.io/agent/chat/) with the agent | | **Versions** | [Releases and rollback](https://docs.bohr.io/agent/versions-and-rollback/) | | **Builds** | The [GitHub Actions runs](https://docs.bohr.io/projects/builds/) with their logs | | **Members** | [Who is in the project](https://docs.bohr.io/projects/members/) and invites | | **Settings** | [General, Domains, Environment variables, Authentication, Agent, Hosting](https://docs.bohr.io/projects/settings/) | The header links to the production site, the preview and the repository on GitHub. ## Overview - **Addresses**: **Production** and **Preview**. The preview is *Available once the agent is on*. - **Agent**: *Working on:* the current request, or *No request in progress.*, the number of requests in the queue, and **Open the agent**. - The preview's state: *The preview matches production.* or what changed, with **Approve and publish** and **Discard**, and **See the preview**. - **Latest version**: the last release and **See the versions**. - **What is missing**: a checklist of what still needs you, for example *The agent is not set up*, *Repository not prepared* (with **Prepare repository**), *No builds yet* or *The project setup failed*. ### The site view Click the site on the Overview to open it in full screen. The bar shows the **Deployment shown** (**Live**, branch, **Last update**, **Hash**, **View logs**), lets you rotate the device and **Open in a new tab**, and **Chat** opens the agent beside the site, switching the view to the preview. Beside the selector, the preview is *Same as production* or has changes (*2 changes*; hover it for what changed). ## Who can do what | | Owner | Member | | --- | --- | --- | | Ask for changes, approve, discard, roll back | Yes | Yes | | Cancel requests | Anyone's | Their own | | Invite and remove members | Yes | No | | Settings (variables, domains, authentication, agent) | Yes | Read only | | Delete the project | Yes | No | In an [organization](https://docs.bohr.io/organizations/overview/), the organization's admins are owners of every project; its members are members. --- # Builds > The GitHub Actions runs of the project's repository, with their jobs and logs. Source: https://docs.bohr.io/projects/builds/ **Builds** lists the GitHub Actions runs of the repository: every deploy, with its steps and logs, without leaving bohr. - Each run shows its title, number, branch, status and when it ran. **View on GitHub** opens it there. - Open a run to see its jobs and each job's **Log**. While a run is in progress, *this page refreshes on its own*. - **Refresh** reloads the list. | Status | Meaning | | --- | --- | | **Queued** | Waiting for a runner | | **Running** | In progress | | **Success** | Finished; for a deploy, production now serves it | | **Failed** | A step failed; open its log | | **Cancelled** | Cancelled on GitHub | | **Skipped** | GitHub skipped it | | **Action required** | GitHub needs someone to act (for example, approve a run) | *No builds yet* means GitHub Actions has not run in the repository. For a new project, the first deploy runs after you approve the agent's [preparation of the repository](https://docs.bohr.io/getting-started/first-deploy/). When a deploy fails, the chat says *"The production deploy failed"* with a link to the run, and production keeps serving the previous version. Ask the agent to fix it, pasting the error line if you want: *"The deploy failed with: …. Fix it."* See also [Troubleshooting](https://docs.bohr.io/reference/troubleshooting/#deploys). --- # Members and roles > Invite people to a project with single-use codes, and manage who has access. Source: https://docs.bohr.io/projects/members/ Everyone in a project can ask for changes and approve releases. Only the **owner** manages keys, settings and access. | Role | Can | | --- | --- | | **Owner** | Everything: settings, variables, domains, authentication, invites, removing members, deleting the project | | **Member** | Ask for changes, approve, discard and roll back, cancel their own requests, see settings | The project's owner is whoever created it. In an [organization](https://docs.bohr.io/organizations/members/), the organization's admins are the owners of all its projects, and its members are members: they do not need invites. ## Invite someone 1. Open **Members** and, under **Invite someone**, click **Create an invite code**. 2. Share the code (**Copy the code**) or the link (**Copy the link**). The code is single use and valid for **24 hours**. 3. The person either: - opens the link, signs in with their own account and the code is already filled in (**Join the project**); or - sends `/join ` to the project's Telegram bot. Codes look like `K7P2-9QXM` and are forgiving when typed by hand: case, spaces and the letters `I`, `L` and `O` are fine. On Telegram, the owner can also send `/invite`. ## Remove someone Click **Remove** next to the person and confirm. They lose access to the project on the web and on Telegram right away, and their waiting requests are cancelled. The owner cannot be removed. On Telegram: `/members` lists people with numbers, and `/revoke ` removes one. --- # Project settings > The General, Agent and Hosting pages of a project's settings. Source: https://docs.bohr.io/projects/settings/ **Settings** has its own menu: **General**, **Domains**, **Environment variables**, **Authentication**, **Agent** and **Hosting**. Members can read them; only the owner changes them. - [Domains](https://docs.bohr.io/projects/domains/) - [Environment variables](https://docs.bohr.io/projects/variables/) - [Authentication](https://docs.bohr.io/projects/password-protection/) ## General The project's details on this instance: | Field | | | --- | --- | | **Name** | The project's name in bohr | | **Repository** | `owner/name` on GitHub | | **Production branch** | Approved changes are merged here, and production deploys from it | | **Production address** | The site's address, or *Not published yet* | | **Hosting** | **Your Cloudflare account**, or **Hosted by bohr** for projects migrated from v1 | | **Agent** | Whether the agent is on | | **Project ID** | The id the [CLI](https://docs.bohr.io/cli/commands/) and the API use, like `p3k9x0a7qd2m` | The bottom of this page has the [Danger zone](https://docs.bohr.io/projects/delete/). ## Agent | Setting | | | --- | --- | | **Model** | The model the agent uses in this project, instead of the one chosen in the account settings | | **Budget per request (US$)** | The agent stops when a request costs this much. Default 5. | | **Maximum turns** | How many rounds the agent can take on one request, from 1 to 1000. Default 200. | | **Agent language** | Language of the agent's and the project bot's replies: Português, English or Español | **Telegram** shows the project bot (**Open in Telegram**) or lets the owner connect one with a BotFather token. See [Telegram](https://docs.bohr.io/agent/telegram/). **Provider and key** live in your account settings and apply to all your projects: **Open the agent settings** ([Model providers](https://docs.bohr.io/agent/providers/)). ## Hosting - **In your Cloudflare account**: the project's site, agent and preview run in your Cloudflare account. Nothing to do. - **Hosted by bohr**: a project migrated from bohr v1, still in bohr's Cloudflare account. This page lets you move it to your own account. See [Move to your Cloudflare account](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/). --- # Environment variables > Site variables for the production Worker, and build variables for the GitHub Actions build, including secrets. Source: https://docs.bohr.io/projects/variables/ **Settings › Environment variables** has two lists. Only the owner changes them; members see the names, and the values of plain variables. ## Site variables They reach the site's **production Worker** at runtime (`env.NAME` in a Worker). - **In your Cloudflare account**, every site variable is stored as a secret of the production Worker, so it survives deploys, and changes apply right away. bohr keeps the plain values only to show them. - **Hosted by bohr** (migrated from v1): secret ones apply right away; plain ones from the next deploy. A site variable can only be set once the site's Worker exists: publish the site once first. If the same name is a plain `vars` entry in your `wrangler.jsonc`, bohr answers *"… is already set in the site's wrangler config: change it there."* ## Build variables They are kept **encrypted** in bohr and handed to GitHub Actions when it installs and builds the site (for example `NEXT_PUBLIC_API_URL`, `SENTRY_AUTH_TOKEN`). They apply from the next deploy. The deploy workflow asks bohr for them at run time, with the run's GitHub OIDC token, together with the Cloudflare credentials. Nothing is stored in your repository's secrets. See [The deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/). ## Adding a variable 1. Click **New variable**. 2. Type the **Name** and **Value**. Names use letters, digits and `_`, starting with a letter or `_`. 3. Check **Secret** for anything sensitive: once saved, its value is hidden and cannot be shown again (*Hidden value — type to replace it*). 4. Click **Save**. Names bohr or GitHub use themselves are refused: anything starting with `GITHUB_`, `CLOUDFLARE_` or `BOHR_`, and `CI`. ## In your code ```ts // A Worker: site variables are on env export default { async fetch(request: Request, env: { RESEND_API_KEY: string }) { // use env.RESEND_API_KEY return new Response('ok'); }, }; ``` Build variables are ordinary environment variables during `install` and `build` (`process.env.NEXT_PUBLIC_API_URL`). :::caution Values in plain `vars` of `wrangler.jsonc` are committed to your repository. Put secrets in **Site variables** marked **Secret**, never in the repository or in the chat. ::: ## Template projects Projects created from a [template](https://docs.bohr.io/getting-started/templates/) have an `ADMIN_PASSWORD` site variable (secret) with the admin area's password. Change it here. --- # Password protection > Ask visitors for a username and password (Basic Auth) before they open the site. Source: https://docs.bohr.io/projects/password-protection/ **Settings › Authentication** puts a username and password (HTTP Basic Auth) in front of your production site. Use it for a site that is not public yet, or for a client's staging site. 1. Under **Method**, choose **Basic Auth** (or **None** to turn protection off). 2. Under **Users**, add at least one **Username** and **Password** (**New user** adds more). Usernames cannot contain spaces or `:`. 3. Optional: under **Open URLs**, add paths that open without a password (**New URL**). Each path starts with `/`: the exact path, or every path starting with it when it ends with `*` (for example `/api/webhooks/*`). 4. Click **Save**. Protection takes effect within a few seconds, without a deploy. To change a password, type a new one; leaving it blank keeps the current one (*Password kept — type to change it*). bohr stores only salted PBKDF2-SHA256 hashes, never the passwords. ## How it works - **In your Cloudflare account**: bohr's deploy workflow wraps your site in a small gate that checks the credentials. The settings live in a secret of your production Worker (`BOHR_SITE_AUTH`), so turning protection on or off needs no deploy. The gate writes `.bohr-gate.mjs` and `wrangler.bohr-gate.json` at deploy time; they are never committed. - **Hosted by bohr**: bohr's edge checks the credentials before your Worker runs. :::caution[Custom deploy commands] The gate only wraps sites deployed with the default command, `npx wrangler deploy`. If your [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/) sets its own `deploy` command, the panel shows *"Protection does not apply to this site"*: bohr cannot put the protection in front of it. ::: The preview has its own, unlisted address and is not covered by this setting. Protection applies to the production site. --- # Custom domains > Attach domains of your Cloudflare account to the production site. Source: https://docs.bohr.io/projects/domains/ **Settings › Domains** attaches domains to your production site as Cloudflare **Custom Domains** of its Worker. Cloudflare creates the DNS record and the certificate automatically. ## Before you start - The site must be **published once**: domains attach to the site's Worker, which the first deploy creates. Until then the page says *"The site is not published yet"*. - The domain must be a **zone in the same Cloudflare account** the project runs in. If the domain is elsewhere, add it to Cloudflare first (and point its nameservers to Cloudflare). If the account has no zones, the page says *"Your Cloudflare account has no domains yet."* ## Add a domain 1. Under **Add a domain**, pick the zone in **Domain on Cloudflare**. 2. Optional: type a **Subdomain** (`www`, `shop`…). Leave it empty for the zone itself. 3. Check *The site will open at …* and click **Add**. It can take a few minutes for the certificate. The page also shows the site's **Worker** and its **workers.dev address**, which keeps working. Wildcards (`*.example.com`) are not accepted. ## Remove a domain Click **Remove** next to it and confirm. The site stops opening at that domain; the DNS record goes with it. ## Projects hosted by bohr Projects migrated from bohr v1 keep the domains they had in v1, served by bohr (each shows **Live** or **Pending**). To change them, [move the project to your Cloudflare account](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/). --- # Delete a project > Remove a project and everything bohr created for it, keeping or deleting the published site. Source: https://docs.bohr.io/projects/delete/ The **Danger zone** at the bottom of **Settings › General** deletes the project. Only the owner can do it, and it cannot be undone. 1. Click **Delete project**. 2. Read what goes and what stays. 3. Optional: check **Also delete the site published in my Cloudflare account**. 4. Type the project's name to confirm and delete. ## What goes - The project's agent: its Workers (the project Worker and the preview Worker) and its container. - The preview, bohr's hosting and bohr's routes, for projects hosted by bohr. - The project in bohr: its members, chat, settings and variables. ## What stays - **The GitHub repository is not changed.** - **The site published in your Cloudflare account stays online**, unless you checked the option above. That option removes the site's production Worker and the `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` Actions secrets older versions of bohr may have left in the repository, so it is not published again. - **The project's Telegram bot keeps existing.** Delete it in @BotFather if you want. The repository's `.github/workflows/bohr-deploy.yml` stays too. Once the project is deleted, bohr no longer gives it deploy credentials, so its runs fail; delete the file if you don't need it. If something could not be removed, the panel says *"The project was deleted, but something was left behind"* with the list, for you to clean up by hand. --- # How deploys work > How a bohr project builds and publishes to Cloudflare, and the files that describe it. Source: https://docs.bohr.io/deploys/how-deploys-work/ A bohr project's production site is a **Cloudflare Worker with static assets**. It is built and published by **GitHub Actions**, from the production branch, after every approved change. ``` .bohr/deploy.json ──(bohr writes)──▶ .github/workflows/bohr-deploy.yml │ on push to the production branch ▼ checkout → setup Node.js → bohr credentials (OIDC) → install → build │ ▼ D1 migrations (if any) → wrangler deploy → your Cloudflare account ``` ## The files | File | Who writes it | What it says | | --- | --- | --- | | [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/) | The agent (you can edit it) | How to build production: folder, Node.js version, package manager, install, build and deploy commands | | [`.github/workflows/bohr-deploy.yml`](https://docs.bohr.io/deploys/deploy-workflow/) | bohr, from `.bohr/deploy.json` | The GitHub Actions workflow that runs on pushes to the production branch | | [`wrangler.jsonc`](https://docs.bohr.io/deploys/wrangler-config/) | The agent or you | The Worker: its name, entry point, static assets, bindings | | [`.bohr/preview.json`](https://docs.bohr.io/deploys/preview-json/) | The agent (you can edit it) | How the preview runs the dev server in the sandbox | ## What triggers a deploy - **Approving a preview**: bohr merges `homolog` into the production branch; the push runs the workflow. - **Rolling back**: the revert is merged; same thing. - **Any push to the production branch**, by you or another tool. - **`bohr deploy --prod`** from the [CLI](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account), by a project owner. In GitHub Actions, only the production branch deploys. The agent's preview is not a deploy: it is the dev server in the agent's sandbox. Owners can also upload preview versions of any branch with `bohr deploy`; those never change production. ## Credentials No Cloudflare token is stored in your repository. When the workflow runs, its first bohr step asks the bohr instance for your Cloudflare credentials and the project's [build variables](https://docs.bohr.io/projects/variables/#build-variables), proving who it is with a **GitHub OIDC token**. bohr only answers runs of `bohr-deploy.yml` on a push (or manual run) to the production branch of a project in its owner's account. See [Security model](https://docs.bohr.io/reference/security/#deploy-credentials). ## Never taking the site down `wrangler deploy` uploads the new version and switches to it in one step. If the build or the deploy fails, production keeps serving the previous version, and the chat links to the failed run. ## Projects hosted by bohr Projects migrated from bohr v1 deploy differently: their v1 workflow keeps working and publishes to bohr's hosting, and every pushed branch gets its own address. They can also be deployed with the [CLI](https://docs.bohr.io/cli/deploy/). See [Migrating from bohr v1](https://docs.bohr.io/migrating-from-v1/what-changes/). --- # The deploy workflow > The GitHub Actions workflow bohr writes from .bohr/deploy.json, step by step. Source: https://docs.bohr.io/deploys/deploy-workflow/ bohr writes `.github/workflows/bohr-deploy.yml` from [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/), from a fixed template. **Change `.bohr/deploy.json`, not the workflow**: bohr rewrites the workflow whenever the agent's work changes the deploy settings, and its first line says so: ```yaml # Written by bohr from .bohr/deploy.json. Change that file instead: bohr rewrites this one. ``` bohr only replaces a `bohr-deploy.yml` that starts with that line. A workflow your team wrote under that name keeps deploying as it is. ## What it looks like For `{"install": "npm ci", "build": "npm run build"}` on the production branch `main` (long scripts shortened): ```yaml name: bohr deploy on: push: branches: ["main"] concurrency: group: bohr-deploy cancel-in-progress: false permissions: contents: read id-token: write jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 30 defaults: run: working-directory: "." steps: - uses: actions/checkout@v5 with: persist-credentials: false - uses: actions/setup-node@v5 with: node-version: "24" - name: Build variables # loads build variables older versions of bohr kept as BOHR_BUILD_ Actions secrets - name: bohr credentials id: bohr-credentials uses: bohr-io/deploy-action@v1 with: credentials: "true" - name: Install run: "npm ci" - name: Build run: "npm run build" - name: D1 migrations # only when the wrangler config has D1 databases with a migrations_dir - name: Deploy # wraps the site in the password gate, then: # npx wrangler deploy --message "bohr" --tag "bohr-${GITHUB_SHA::12}" env: CLOUDFLARE_API_TOKEN: ${{ steps.bohr-credentials.outputs.cloudflare-api-token }} CLOUDFLARE_ACCOUNT_ID: ${{ steps.bohr-credentials.outputs.cloudflare-account-id }} ``` ## The steps | Step | What it does | | --- | --- | | Trigger | Only pushes to the production branch. Pushes there only come from approved merges, rollbacks, or people with write access. | | `concurrency` | One deploy at a time; a new push waits for the running one instead of cancelling it. | | `permissions` | `contents: read` and `id-token: write` (to request the GitHub OIDC token). Nothing else. | | Setup | `oven-sh/setup-bun@v2` when the package manager is `bun`; `actions/setup-node@v5` with the `node` version; `actions/setup-python@v6` when `python` is set; `corepack enable` for `pnpm` and `yarn`. | | **bohr credentials** | [`bohr-io/deploy-action@v1`](https://docs.bohr.io/github-action/reference/#credentials-mode) with `credentials: "true"`: asks bohr for the project's [build variables](https://docs.bohr.io/projects/variables/#build-variables) (exported, masked, for the next steps) and the Cloudflare token and account (step outputs, for the deploy step only). Self-hosted instances add their `instance-url`. | | **Install**, **Build** | Your commands, in `directory`. The repository secrets listed in `secrets` are passed to these two steps only. | | **D1 migrations** | `npx wrangler d1 migrations apply --remote` for each D1 database with a `migrations_dir`. See [D1 databases](https://docs.bohr.io/deploys/d1-databases/). | | **Deploy** | With the default command, wraps the site in the [password gate](https://docs.bohr.io/projects/password-protection/) and runs `npx wrangler deploy`. A custom `deploy` command runs as it is. The Cloudflare token reaches this step (and the migrations) only. | ## Why it is safe - The commands are single lines without GitHub expressions (`${{ }}`), so they cannot read other secrets or contexts. - The Cloudflare token never lives in the repository: bohr hands it only to runs of this workflow, on a push or a manual run (`workflow_dispatch`) on the production branch. A pull request, another branch or another workflow gets nothing. - The agent cannot edit workflows; bohr writes this one itself. ## Running it again Re-run a failed run on GitHub (**Re-run jobs**); the [Builds](https://docs.bohr.io/projects/builds/) page links to each run. A re-run of a push to the production branch gets the credentials again. ## Changing it by hand Edit `.bohr/deploy.json` and let the agent's next change regenerate the workflow, or ask the agent directly: *"Change the build command in .bohr/deploy.json to …"*. If you need a workflow bohr cannot generate, write your own `.github/workflows/bohr-deploy.yml` without bohr's first line: bohr leaves it alone. It can still get the Cloudflare credentials with the [credentials step](https://docs.bohr.io/github-action/reference/#credentials-mode), since bohr checks the workflow's path and branch, not its contents. --- # .bohr/deploy.json > Reference of the file that describes how bohr builds and deploys production. Source: https://docs.bohr.io/deploys/deploy-json/ `.bohr/deploy.json` tells bohr how to build and deploy your production site. The agent writes it when it prepares the repository and keeps it working; you can edit it too. bohr generates [the deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/) from it, and the [CLI](https://docs.bohr.io/cli/deploy/) runs its `install` and `build` before deploying. ```json { "directory": ".", "node": "24", "packageManager": "npm", "install": "npm ci", "build": "npm run build" } ``` ## Fields | Field | Default | Description | | --- | --- | --- | | `directory` | `"."` | Folder with the project's `package.json` and wrangler config, relative to the repository root. `"."` or a relative path like `"apps/web"`: no `..`, no folders starting with a dot. | | `node` | `"24"` | Node.js major version for the build, two digits (`"22"`, `"24"`). | | `packageManager` | `"npm"` | `npm`, `pnpm`, `yarn` or `bun`. `pnpm` and `yarn` come from Corepack; `bun` is set up with `oven-sh/setup-bun`. | | `python` | — | Python version when the build needs it (Python Workers), like `"3.12"`. | | `install` | — | Installs dependencies, e.g. `"npm ci"`. Omit to skip. | | `build` | — | Builds production, e.g. `"npm run build"`. Omit to skip. | | `deploy` | `"npx wrangler deploy"` | Deploys the build to your Cloudflare account. Keep the default unless the framework needs its own command (for example `"npx opennextjs-cloudflare deploy"`): [password protection](https://docs.bohr.io/projects/password-protection/) only applies with the default. | | `secrets` | `[]` | Names of **GitHub repository secrets** the install and build steps need, e.g. `["SENTRY_AUTH_TOKEN"]`. Up to 50. | ### Rules for commands `install`, `build` and `deploy` are single shell lines of up to 500 characters, without line breaks and without GitHub expressions (`${{ … }}`). Chain commands with `&&`. ### Rules for secrets Names use `A-Z`, `0-9` and `_`. Names starting with `GITHUB_` (reserved by GitHub) or `CLOUDFLARE_` (the Cloudflare credentials only go to the deploy step, which bohr writes) are refused. You rarely need `secrets`: prefer [build variables](https://docs.bohr.io/projects/variables/#build-variables), which you set in the bohr panel and which reach install and build without touching your repository's settings. Use `secrets` for values you already keep as GitHub Actions secrets. ## When it is invalid If the file has a problem, the deploy workflow is not updated and the chat says: *"The file .bohr/deploy.json has a problem (…), so the production deploy was not updated. Ask me to fix it."* The CLI stops with `.bohr/deploy.json is invalid: …` (exit code 2). ## Examples A static site built with Vite: ```json { "install": "npm ci", "build": "npm run build" } ``` A pnpm monorepo app: ```json { "directory": "apps/web", "packageManager": "pnpm", "install": "pnpm install --frozen-lockfile", "build": "pnpm build" } ``` Next.js with OpenNext: ```json { "node": "22", "install": "npm ci", "build": "npx opennextjs-cloudflare build", "deploy": "npx opennextjs-cloudflare deploy" } ``` --- # .bohr/preview.json > Reference of the file that tells bohr how to run the preview's dev server in the agent's sandbox. Source: https://docs.bohr.io/deploys/preview-json/ The [preview](https://docs.bohr.io/agent/previews/) is your project's **dev server**, running in the agent's sandbox from the `homolog` branch. `.bohr/preview.json` says how to start it. The agent writes it when it prepares the repository and keeps it working. ```json { "install": "npm ci", "command": "npm run dev -- --host 0.0.0.0 --port 8080", "port": 8080, "env": {} } ``` ## Fields | Field | Required | Description | | --- | --- | --- | | `command` | Yes | The long-running dev server command (up to 1000 characters). Runs with `bash -lc` in the repository root. | | `port` | Yes | The port the dev server listens on, 1024 to 65535. Port `4840` is reserved for the bohr agent. | | `install` | No | Installs dependencies. Runs at the first start and whenever a dependency manifest or lockfile changes. | | `env` | No | Extra environment variables for `install` and `command`. Never secrets: this file is committed. | The commands also get `PORT` (the port), `HOST=0.0.0.0`, `BROWSER=none` and `CI=1`. ## Requirements for the dev server - Listen on `0.0.0.0` (all interfaces) at `port`. Dev servers that only listen on `localhost` also work. - Accept any `Host` header. Vite, for example, needs `server.allowedHosts: true`. - Be ready within **3 minutes**. Otherwise the preview is marked failed with the end of its output. - `install` must finish within 15 minutes. ## When it restarts - After every request and every sync, the preview restarts if `.bohr/preview.json` or the dependencies changed (`package.json`, lockfiles, `requirements*.txt`, `pyproject.toml`, `go.mod`, `Cargo.toml` and similar, anywhere in the repository), or if it is not running. Otherwise the dev server's own reload picks up the change. - Stopping kills the whole process group: whatever the command started stops with it. ## States The preview goes through these states: | State | Meaning | | --- | --- | | `starting` | Installing or waiting for the port | | `running` | The port accepts connections | | `failed` | The command exited, or the port did not open in time; the log shows the end of the output | | `stopped` | No checkout yet, no `.bohr/preview.json`, or an invalid one | Messages you may see: *"The preview is not set up yet: the agent prepares it when it imports the repository."* (no file yet), and *"The preview could not start. Ask the agent to check the file .bohr/preview.json."* ## Examples A Worker with a D1 database (the templates): ```json { "install": "npm ci", "command": "npx wrangler d1 migrations apply DB --local && npx wrangler dev --ip 0.0.0.0 --port 8080", "port": 8080 } ``` Astro: ```json { "install": "npm ci", "command": "npx astro dev --host 0.0.0.0 --port 4321", "port": 4321 } ``` --- # Wrangler config and bindings > The wrangler config bohr deploys, the bindings each kind of project supports, and what is ignored. Source: https://docs.bohr.io/deploys/wrangler-config/ Your site is described by a standard Cloudflare **wrangler config**, `wrangler.jsonc` (or `wrangler.json`), in the folder `.bohr/deploy.json` points at. The agent adds one if the repository has none. ```jsonc { "name": "my-site", "main": "src/index.ts", "compatibility_date": "2026-10-01", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist", "binding": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] } } ``` `name` is the name of your production Worker. bohr uses it to find the site for [variables](https://docs.bohr.io/projects/variables/), [password protection](https://docs.bohr.io/projects/password-protection/) and [domains](https://docs.bohr.io/projects/domains/). ## Projects in your own Cloudflare account They deploy with `wrangler deploy` in your account, so **every binding wrangler supports works**: D1, KV, R2, Durable Objects, Queues, Workflows, Containers, Hyperdrive, AI, cron triggers, routes… Resources a binding refers to must exist in your account (the agent can create them, or you do it in the dashboard). Things to know: - `vars` in the config are committed to the repository; secrets go in **Site variables** marked **Secret**. - With [password protection](https://docs.bohr.io/projects/password-protection/) on, the deploy wraps your Worker in a gate and sets `assets.run_worker_first` to `true`, so static files pass through it too. - D1 migrations run before each deploy: see [D1 databases](https://docs.bohr.io/deploys/d1-databases/). ## Projects hosted by bohr Projects migrated from bohr v1 (until they [move](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/)) deploy to bohr's hosting, which supports a subset: | Key | Supported | | --- | --- | | `main` | Yes. Bundled with `wrangler deploy --dry-run`. Leave it out for a static site. | | `compatibility_date` | Required | | `compatibility_flags` | Yes | | `vars` | String values only | | `assets.directory`, `binding`, `not_found_handling`, `html_handling`, `run_worker_first` | Yes, as in wrangler | | Secrets | Yes, as [site variables](https://docs.bohr.io/projects/variables/) | | Any other binding (`kv_namespaces`, `d1_databases`, `r2_buckets`, `durable_objects`, `queues`, `services`, `hyperdrive`, `ai`…) | No: the deploy stops with *"bohr.io Cloud managed projects support static assets, vars and secrets for now; … is not supported."* | | `routes`, `triggers.crons`, `env` sections, `assets.base_path` | Ignored, with a warning | Also for these projects: - `wrangler.toml` is not read: convert it to `wrangler.jsonc` (same keys, in JSON with comments). - A Cloudflare Pages config (`pages_build_output_dir`) is refused: use `"assets": { "directory": "…" }`. - When the build leaves `.wrangler/deploy/config.json` pointing at a generated config (as the Cloudflare Vite plugin does), that config is used. ## Static assets For any project, the files in `assets.directory` follow wrangler's rules: - `.assetsignore` at the root of the folder excludes files (gitignore syntax). - `_redirects` and `_headers` at the root are applied as routing rules, not served as files (up to 256 KiB each when hosted by bohr). - Each file can be at most **25 MiB** (Cloudflare's limit). - bohr's CLI and action never publish `.git`, `.env` or `.env.` files or folders, whatever `.assetsignore` says. --- # D1 databases and migrations > Use Cloudflare D1 in a bohr project, and how migrations run on each deploy. Source: https://docs.bohr.io/deploys/d1-databases/ Projects in your own Cloudflare account can use **D1**, Cloudflare's SQL database. [Template](https://docs.bohr.io/getting-started/templates/) projects come with one. ## The binding ```jsonc { "d1_databases": [ { "binding": "DB", "database_name": "my-site", "database_id": "", "migrations_dir": "migrations" } ] } ``` In the Worker, the database is `env.DB`. ## Migrations Put schema changes in numbered SQL files in `migrations_dir`, one per change: ``` migrations/ 0001_init.sql 0002_add_city.sql ``` On every production deploy, **before** `wrangler deploy`, the [deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/) runs, for each D1 database that declares a `migrations_dir` the repository has: ```sh npx wrangler d1 migrations apply --remote ``` D1 records which migrations it applied, so each one runs once. The workflow only gets this step when the config has such databases. Ask the agent for schema changes in plain words (*"Save the visitor's city with each sign-up"*): it adds a new migration file instead of changing old ones. ## The preview The preview runs `wrangler dev`, with a **local** copy of the database in the sandbox. Template previews apply the migrations locally first: ```sh npx wrangler d1 migrations apply DB --local && npx wrangler dev --ip 0.0.0.0 --port 8080 ``` Preview data is separate from production and is lost when the sandbox sleeps. Previews deployed with [`bohr deploy`](https://docs.bohr.io/cli/deploy/#previews) are different: they are versions of the production Worker, with the same bindings, so they **use the production databases**, and no migrations are applied for them. `bohr deploy --prod` applies the migrations like the workflow, before deploying. ## Permissions Creating a database needs the **D1: Edit** permission on your Cloudflare token (bohr asks for it when you create a project from a template). Applying migrations in the deploy uses the same token. :::caution A [rollback](https://docs.bohr.io/agent/versions-and-rollback/) reverts code, not data: migrations that already ran stay applied. To undo a schema change, ask the agent for a new migration that reverses it. ::: --- # Monorepos and working directories > Deploy a project that lives in a subfolder of its repository. Source: https://docs.bohr.io/deploys/monorepos/ A project does not have to be at the repository root. Point `directory` in [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/) at the folder with the site's `package.json` and wrangler config: ```json { "directory": "apps/web", "packageManager": "pnpm", "install": "pnpm install --frozen-lockfile", "build": "pnpm build" } ``` - The deploy workflow runs every step (`install`, `build`, migrations and `deploy`) in that folder (`defaults.run.working-directory`). - Workspace tools find the repository root from there: `pnpm install` in `apps/web` installs the whole workspace. - bohr finds the production Worker's name in that folder's `wrangler.jsonc`. - The [CLI](https://docs.bohr.io/cli/deploy/) reads `.bohr/deploy.json` at the repository root and deploys from `directory`, or from `--dir`. `.bohr/deploy.json` and `.bohr/preview.json` themselves always live at the repository root. ## The preview `.bohr/preview.json` commands run from the repository root. Change directory in the command when needed: ```json { "install": "pnpm install", "command": "pnpm --filter web dev --host 0.0.0.0 --port 5173", "port": 5173 } ``` ## One repository, one project Each repository can be one bohr project. To deploy two sites from one repository, split them into two repositories, or deploy the second one yourself with its own workflow. --- # Install the CLI > Install the bohr CLI (npm package bohr) or run it with npx. Source: https://docs.bohr.io/cli/install/ The **bohr CLI** deploys bohr projects from your terminal, from scripts, from CI and from AI coding agents. It is the same code that runs inside the [`bohr-io/deploy-action`](https://docs.bohr.io/github-action/reference/) GitHub Action. It needs **Node.js 20 or later**. ## Run it with npx No install needed; always the latest release: ```sh npx bohr@latest --version npx bohr@latest deploy ``` ## Install it globally ```sh npm install --global bohr bohr --version ``` Update with `npm install --global bohr@latest`. ## As a project dependency ```sh npm install --save-dev bohr npx bohr deploy ``` ## First steps ```sh bohr login # once per computer: approve it in the browser bohr projects # the projects you can deploy bohr deploy # build and deploy a preview of the current branch ``` ## Help ```sh bohr --help # every command, global options, exit codes bohr deploy --help # one command's options bohr help deploy # the same ``` :::caution[bohr v1] This is bohr v2's CLI (version 4.x on npm: versions 2.x and 3.x there belong to an unrelated 2016 package). In a repository still on bohr v1 it hands the command, unchanged, to bohr v1's CLI (`bohr@1.0.577`), so v1 workflows that run `npx bohr@latest deploy` keep deploying as before; the same goes for bohr v1 commands such as `bohr dev`. See [Migrating from bohr v1](https://docs.bohr.io/migrating-from-v1/overview/). ::: :::note[Projects in your own Cloudflare account] `bohr deploy` publishes projects that **bohr hosts** (projects migrated from bohr v1) and, since CLI 4.1.0, projects in your own Cloudflare account, which only their owners can deploy from the CLI: wrangler deploys them with the owner's Cloudflare connection. See [Projects in your own Cloudflare account](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account). The other commands work with every project. ::: --- # Authentication > Sign the CLI in with bohr login, use a token from the panel with BOHR_TOKEN, or GitHub OIDC in Actions. Source: https://docs.bohr.io/cli/authentication/ The CLI authenticates in one of three ways, in this order: 1. **`BOHR_TOKEN`**, when set: a token from the panel. For scripts, CI and AI agents. 2. **The login stored by `bohr login`**, for that instance. For people at their computer. 3. **GitHub OIDC**, in GitHub Actions without `BOHR_TOKEN` (needs `permissions: id-token: write`). ## bohr login ```sh bohr login ``` The CLI prints a link and a code, and opens the browser: ``` To sign in, open https://app.bohr.io/cli?code=BCDF-GHJK and approve "CLI on my-laptop" with the code BCDF-GHJK. ``` In the panel (**Sign in to the bohr CLI**), check that the code matches your terminal and click **Approve**. Approve only a sign-in you started yourself: someone who sends you such a link could otherwise deploy as you. The code is valid for 10 minutes. The CLI then keeps a token of its own, named **"CLI on <your computer>"**, with access to all your projects and no expiry. It is stored in `credentials.json` in your config folder, readable by you only: | System | Folder | | --- | --- | | Linux | `$XDG_CONFIG_HOME/bohr` or `~/.config/bohr` | | macOS | `~/Library/Application Support/bohr` | | Windows | `%APPDATA%\bohr` | `BOHR_CONFIG_DIR` overrides the folder. One login is kept per instance. ```sh bohr whoami # who the CLI acts as, and with which token bohr logout # revokes this computer's token and forgets it ``` ## Tokens for scripts and agents Create a token in the panel, **Account › Tokens** ([details](https://docs.bohr.io/account/tokens/)): a name, the projects it may use, and an expiry. The panel shows it once. Then: ```sh export BOHR_TOKEN=bohr_… bohr deploy --json ``` - Tokens start with `bohr_`. Anything else in `BOHR_TOKEN` is refused (exit code 3). - A token acts as you, on the projects you are a member of, limited to the ones it lists. It never carries an instance administrator role. - `BOHR_TOKEN` wins over the stored login. `bohr login` warns you when it is set. - Revoke it in the panel at any time; scripts using it stop at once (exit code 3). ## GitHub Actions with OIDC In a workflow with `permissions: id-token: write` and no `BOHR_TOKEN`, `bohr deploy` signs in with the run's GitHub OIDC token and deploys **the run's repository and pushed branch**, like the deploy action. `--prod`, `--branch` and `--project` need `BOHR_TOKEN` instead. See [CI usage](https://docs.bohr.io/cli/ci/). ## Self-hosted instances Every command takes `--instance `, or the `BOHR_INSTANCE_URL` variable. The default is `https://app.bohr.io`. The address must use `https` (except `localhost`). ```sh bohr login --instance https://bohr.example.com ``` --- # bohr deploy > Build and deploy a folder as a preview of a branch, or as production with --prod. Source: https://docs.bohr.io/cli/deploy/ ```sh bohr deploy [--prod] [--branch ] [--project ] [--dir ] [--no-build] [--yes] [--json] [--instance ] ``` Builds the project and publishes it. **Previews by default**; production only with `--prod`. ## Options | Option | Description | | --- | --- | | `--prod` | Deploy production (project owners only). | | `--branch ` | The preview's branch. Default: the checked-out branch. | | `--project ` | The project id (see `bohr projects`). Default: the folder's link, or the GitHub remote. | | `--dir ` | Folder with the wrangler config. Default: `directory` from `.bohr/deploy.json`, or the current folder. | | `--no-build` | Deploy what is already built: skip `.bohr/deploy.json`'s install and build. | | `-y`, `--yes` | Do not ask before deploying production. | | `--json` | Print one JSON object with the result ([schema](https://docs.bohr.io/cli/json-output/#bohr-deploy)). | | `--instance ` | The bohr instance. Default: `BOHR_INSTANCE_URL`, the folder's link, or `https://app.bohr.io`. | | `-h`, `--help` | Show the command's help. | ## What it does 1. **Which project**: `--project `, else the folder's link (`.bohr/project.json`, written by [`bohr link`](https://docs.bohr.io/cli/commands/#bohr-link)), else the project of the repository's GitHub remote (`origin`). 2. **Which branch**: `--branch`, else the checked-out branch. On the **production branch** without `--prod`, it publishes a preview on `-preview` instead, and says so. 3. **Build**: runs `install` and `build` from [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/) in its folder, unless `--no-build`. Your environment is the build's: build variables kept in bohr are not downloaded. It warns when your Node.js major version differs from `node`, or when a name in `secrets` is not set. 4. **Bundle**: the Worker is bundled with `wrangler deploy --dry-run` (needs no Cloudflare credentials). Add `wrangler` to your `devDependencies` to pin its version. 5. **Upload**: static files are hashed like wrangler does; only new or changed files are uploaded. 6. **Publish**: the URL is printed on stdout. Progress goes to stderr. Steps 3 to 6 are those of a project **bohr hosts**. A project in its owner's Cloudflare account builds with its build variables and deploys with wrangler instead: see [Projects in your own Cloudflare account](#projects-in-your-own-cloudflare-account). ```console $ bohr deploy Deploying a preview of feature/new-menu of my-site (p3k9x0a7qd2m) on https://app.bohr.io. … 42 files (1.3 MiB), 3 uploaded; 1 Worker module. https://feature-new-menu-my-site-acme.bohr.io ``` ## Production ```sh bohr deploy --prod ``` - Only the project's **owners** (admins of its organization) may deploy production. Others get exit code 5 (`production_not_allowed`). - At a terminal, the CLI asks `Deploy this folder to production (…)? [y/N]` (`… in the owner's Cloudflare account? [y/N]` for a project there). `--yes` skips the question; without a terminal, or with `--json`, it never asks: `--prod` itself is the confirmation. - `--prod` with `--branch` other than the production branch is a usage error. - A project whose production address bohr v1 still serves is left to its repository's workflow, whose first production deploy moves the address (`production_on_v1`, exit code 5). Deploy previews until then. ## Nothing goes down What is served only changes once every file is uploaded and Cloudflare accepted the new version. Any failure before that leaves the previous version online. The final step is never retried, so a deploy cannot publish twice. For projects in your own Cloudflare account, see [below](#never-takes-a-site-down). ## What is never published `.git` and `.env` files or folders (`.env`, `.env.production`…), at any depth, whatever `.assetsignore` says. Each skipped `.env` is reported with a warning. ## Which projects `bohr deploy` publishes both kinds of bohr v2 projects: - **Projects bohr hosts** (migrated from bohr v1) go up through bohr's upload path, as described above. - **Projects in their owner's Cloudflare account** are deployed by wrangler into that account, [as described below](#projects-in-your-own-cloudflare-account). Only their owners can deploy them from the CLI. CLI 4.0.0 and earlier refused the second kind with exit code 5 (`own_account`). If you see that code, update the CLI: `npx bohr@latest`. A folder that is not a bohr v2 project (no wrangler config, no `.bohr/project.json`, no `.bohr/deploy.json`) stops with *"This repository is not a bohr v2 project"* (exit code 4); repositories still on bohr v1 are recognized, and their command goes to bohr v1's CLI unchanged. ## Projects in your own Cloudflare account A project in its owner's Cloudflare account (`own account` in [`bohr projects`](https://docs.bohr.io/cli/commands/#bohr-projects), `mode: "byo"` in its JSON) is deployed with wrangler, in that account, much like its [deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/) does. It needs CLI 4.1.0 or later, Node.js 20 or later and `npx`. ### Who can deploy Only the project's **owners** (for an organization's project: the organization's admins) can deploy it from the CLI, **previews included**: wrangler deploys it with the Cloudflare API token its owner connected to bohr. Members get exit code 5 (`own_account_not_allowed`); they push a branch instead, and the project's workflow deploys the production branch. The token (`BOHR_TOKEN` or `bohr login`) must also reach the project: a token limited to some projects must list it. ### What runs 1. **Credentials**: the CLI asks the instance for the owner's Cloudflare API token and account id, for this deploy only. 2. **Build**: unless `--no-build`, the `install` and `build` commands of [`.bohr/deploy.json`](https://docs.bohr.io/deploys/deploy-json/) run with the project's [build variables](https://docs.bohr.io/projects/variables/#build-variables) kept in bohr in their environment (for the deployed branch: the project-wide values, overridden by the branch's). The build never gets the Cloudflare token or `BOHR_TOKEN`. 3. **Protection**: when `.bohr/deploy.json` uses the default `deploy` command, bohr's protection gate wraps the site as in the deploy workflow, so [password protection](https://docs.bohr.io/projects/password-protection/) covers previews too. 4. **wrangler**: the project's own wrangler when it has one installed, otherwise `npx wrangler@4.149.0`. Previews need wrangler 4.21.0 or later (it added preview aliases): with an older wrangler in the project, previews run 4.149.0. ### Previews ```sh bohr deploy ``` uploads a new version of the Worker with `wrangler versions upload --preview-alias --message "bohr" --tag "bohr-"`. A version upload **never changes what production serves**. - The alias comes from the branch: lowercase letters, digits and dashes, starting with a letter, shortened so that `-` fits in a 63-character DNS label. - The URL printed is the alias's preview URL, else the version's own preview URL. - Previews have an address only when the Worker has preview URLs turned on (`"preview_urls": true` in its wrangler config). Without them, the CLI warns and prints the version it uploaded instead (`url` is `null` in the [JSON](https://docs.bohr.io/cli/json-output/#bohr-deploy)). :::caution[D1 in previews] A preview version has the same bindings as production: its [D1 databases](https://docs.bohr.io/deploys/d1-databases/) **are production's databases**, and D1 migrations are not applied for previews. A preview of a schema change runs against production's current schema and data. Be careful with previews that write data or that depend on a migration. ::: ### Production ```sh bohr deploy --prod ``` 1. At a terminal it asks `Deploy this folder to production (…) in the owner's Cloudflare account? [y/N]`; `--yes` skips the question, and without a terminal or with `--json`, `--prod` itself is the confirmation. 2. **D1 migrations**, as in the deploy workflow: `wrangler d1 migrations apply --remote` for each D1 database in `wrangler.jsonc` (or `wrangler.json`) that declares a `migrations_dir` the folder has. 3. **Deploy**: `wrangler deploy --message "bohr" --tag "bohr-"`. If `.bohr/deploy.json` has a custom `deploy` command, production runs that command instead, with the credentials in its environment, and the protection gate is not added (like the workflow). bohr reads the Worker's name from the wrangler config on the production branch. If the folder's wrangler config names a different Worker, `--prod` stops before deploying with exit code 5 (`worker_mismatch`); a preview only warns. ### Never takes a site down - A preview never touches production. - If a D1 migration fails, the Worker is not deployed: production keeps serving the previous version. - If `wrangler deploy` fails, Cloudflare keeps serving the previous version. ### Commit and tag The tag is `bohr-` when the working tree has no uncommitted changes to tracked files. Otherwise it is `bohr-local`, and no commit is recorded with the deploy. ### Credentials and secrets - The Cloudflare token goes only into the environment of the wrangler processes (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`). It is never printed, logged, put on a command line or written to disk, and error messages show it as `***`. In GitHub Actions it is also masked with `::add-mask::`. - Every time the instance hands out the credentials, its audit log records the user, the token and the project. At most 30 per user and project in 10 minutes; past that the instance answers HTTP 429 (exit code 1): wait a few minutes. - bohr hands out only the owner's own Cloudflare connection, never the token of bohr's own account, and never for projects bohr hosts (those keep deploying through bohr's upload path). ### History The CLI records each deploy in bohr: branch, production or preview, Worker name, version id, URL and commit (`deploymentId` in the JSON). If recording fails, the deploy still succeeded and the CLI only warns. The panel does not show these records yet; project members can read them from the instance's API (`GET /api/projects//cli-deploys`). The Cloudflare dashboard also lists the Worker's versions, with the message `bohr` and the `bohr-…` tag. --- # Commands > Every bohr CLI command with its options and examples. Source: https://docs.bohr.io/cli/commands/ ``` bohr [options] ``` | Command | What it does | | --- | --- | | [`deploy`](https://docs.bohr.io/cli/deploy/) | Build and deploy this folder (a preview, or production with `--prod`) | | [`login`](#bohr-login) | Sign in from this computer (approve it in the browser) | | [`logout`](#bohr-logout) | Sign out: revoke this computer's token and forget it | | [`whoami`](#bohr-whoami) | Show who the CLI acts as, and with which token | | [`projects`](#bohr-projects) | List the projects the token can reach | | [`link`](#bohr-link) | Link this folder to a project (`.bohr/project.json`) | | [`open`](#bohr-open) | Print the project's URL and open it in the browser | ## Options for every command | Option | Description | | --- | --- | | `--instance ` | The bohr instance (default: `$BOHR_INSTANCE_URL`, the folder's link, or `https://app.bohr.io`) | | `--json` | Print one JSON object with the result, for scripts and AI agents ([schemas](https://docs.bohr.io/cli/json-output/)) | | `-h`, `--help` | Show the help | | `-v`, `--version` | Print the version (`bohr --version`) | Unknown options and extra arguments are usage errors (exit code 2). ## bohr login ```sh bohr login [--instance ] ``` Starts a sign-in, prints the link and code, opens the browser (at a terminal) and waits until you approve it in the panel. Stores a token named "CLI on <computer>". See [Authentication](https://docs.bohr.io/cli/authentication/#bohr-login). Fails with exit code 3 when the sign-in is denied (`login_denied`) or the code expires after 10 minutes (`login_expired`). ## bohr logout ```sh bohr logout [--instance ] ``` Revokes the stored token on the instance and deletes it from `credentials.json`. If the instance cannot be reached, it still forgets the token and tells you to revoke it in the panel. Warns when `BOHR_TOKEN` is still set. ## bohr whoami ```sh bohr whoami [--instance ] [--json] ``` ```console $ bohr whoami Ada Lovelace on https://app.bohr.io Token "deploy agent" (BOHR_TOKEN): 2 selected projects, expires 2027-01-10T12:00:00.000Z ``` ## bohr projects ```sh bohr projects [--instance ] [--json] ``` Lists the projects the token reaches: ```console $ bohr projects ID NAME REPOSITORY HOSTED ROLE URL p3k9x0a7qd2m my-site acme/my-site bohr owner https://my-site-acme.bohr.io p8b2m4c6d1ef shop acme/shop own account member - ``` **HOSTED** is `bohr` for projects bohr hosts and `own account` for projects in their owner's Cloudflare account. `bohr deploy` publishes both; projects in their owner's account only for their owners ([details](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account)). ## bohr link ```sh bohr link [--project ] [--instance ] [--json] ``` Writes `.bohr/project.json` at the repository root, so `bohr deploy` and `bohr open` know the project without a GitHub remote. Without `--project`, it uses the project of the `origin` remote. The file records the instance too, when it is not the default. ```json { "projectId": "p3k9x0a7qd2m" } ``` Commit it if everyone working on the folder should deploy to the same project. ## bohr open ```sh bohr open [--branch ] [--print] [--project ] [--instance ] [--json] ``` Prints the production URL (or `--branch`'s preview) and opens it in the browser. `--print` only prints it. Fails with exit code 4 (`no_address`) when that branch was never deployed. ```sh bohr open --branch feature/new-menu --print ``` ## Debugging Set `BOHR_DEBUG=1` to print the stack trace of an error on stderr. --- # JSON output > The --json output of every command, for scripts and AI agents. Source: https://docs.bohr.io/cli/json-output/ With `--json`, a command prints **exactly one JSON object on stdout** and nothing else there; progress and logs go to stderr. It also never asks questions and never opens a browser. ## Success and failure Success: ```json { "ok": true, "...": "the command's fields" } ``` Failure: ```json { "ok": false, "error": { "code": "production_not_allowed", "message": "Only owners of my-site (admins of its organization) can deploy production. Deploy a preview: bohr deploy", "exitCode": 5 } } ``` The process exits with the same `exitCode` ([exit codes](https://docs.bohr.io/cli/exit-codes/)). Branch on `error.code`, not on the message. ## bohr deploy ```json { "ok": true, "instanceUrl": "https://app.bohr.io", "url": "https://feature-new-menu-my-site-acme.bohr.io", "deploymentId": "d_…", "project": { "id": "p3k9x0a7qd2m", "name": "my-site" }, "branch": "feature/new-menu", "production": false, "mode": "managed", "files": 42, "bytes": 1363148, "uploadedFiles": 3, "uploadedBytes": 20480, "modules": 1 } ``` | Field | Description | | --- | --- | | `url` | Where the deployed branch is served | | `deploymentId` | The deployment's id on the instance | | `project` | The project, or `null` in GitHub Actions with OIDC | | `branch`, `production` | What was published (`null` with OIDC) | | `mode` | `managed`: a project bohr hosts. `byo`: a project in its owner's Cloudflare account (below) | | `files`, `bytes` | Static files in the assets folder, and their total size | | `uploadedFiles`, `uploadedBytes` | What actually went up (new or changed files) | | `modules` | Worker modules (0 for a static site) | ### Projects in your own Cloudflare account A project in its owner's Cloudflare account is [deployed with wrangler](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account), so the upload counts are `null` and wrangler's version comes instead: ```json { "ok": true, "instanceUrl": "https://app.bohr.io", "url": "https://feature-new-menu-my-site.acme.workers.dev", "deploymentId": "…", "project": { "id": "p8b2m4c6d1ef", "name": "my-site" }, "branch": "feature/new-menu", "production": false, "mode": "byo", "versionId": "…", "previewUrl": "https://1a2b3c4d-my-site.acme.workers.dev", "workerName": "my-site", "commitSha": "…", "files": null, "bytes": null, "uploadedFiles": null, "uploadedBytes": null, "modules": null } ``` | Field | Description | | --- | --- | | `url` | A preview's alias URL (else the version's preview URL), or production's address. `null` for a preview of a Worker without preview URLs | | `deploymentId` | bohr's record of the deploy, or `null` if it could not be recorded (the deploy still succeeded) | | `versionId` | The Worker version wrangler uploaded or deployed | | `previewUrl` | The version's own preview URL, when wrangler printed one | | `workerName` | The Worker deployed | | `commitSha` | The commit deployed, or `null` when the working tree had uncommitted changes (tag `bohr-local`) | ## bohr projects ```json { "ok": true, "instanceUrl": "https://app.bohr.io", "projects": [ { "id": "p3k9x0a7qd2m", "name": "my-site", "repository": "acme/my-site", "productionBranch": "main", "mode": "managed", "status": "active", "role": "owner", "productionUrl": "https://my-site-acme.bohr.io", "hostnames": [ { "hostname": "my-site-acme.bohr.io", "branch": "main", "isProduction": true, "status": "live" } ] } ] } ``` | Field | Values | | --- | --- | | `mode` | `managed`: bohr hosts it. `byo`: the owner's Cloudflare account (only its owners can `bohr deploy` it). | | `status` | `active`, `migrating` or `suspended` | | `role` | `owner` (may deploy production) or `member` | | `hostnames[].status` | `live`, or `pending` while bohr v1 still serves it | ## bohr whoami ```json { "ok": true, "instanceUrl": "https://app.bohr.io", "user": { "id": "u_…", "name": "Ada Lovelace", "email": "ada@example.com" }, "token": { "id": "tok_…", "name": "deploy agent", "projects": ["p3k9x0a7qd2m"], "expiresAt": "2027-01-10T12:00:00.000Z", "source": "BOHR_TOKEN" } } ``` `token.projects` is `null` for a token that reaches all your projects; `source` is `BOHR_TOKEN` or `bohr login`. ## Other commands | Command | Fields after `"ok": true` | | --- | --- | | `bohr link` | `instanceUrl`, `project` (as in `projects`), `path` (the file written) | | `bohr open` | `instanceUrl`, `url`, `project` (`{ id, name }`), `branch` | | `bohr login` | `instanceUrl`, `user` | | `bohr logout` | `instanceUrl`, `signedOut` (`false` when there was no login) | ## Parsing it ```sh out=$(bohr deploy --json) status=$? if [ "$status" -eq 0 ]; then echo "Deployed to $(echo "$out" | jq -r .url)" else echo "Failed: $(echo "$out" | jq -r .error.code)" >&2 fi ``` --- # Exit codes and errors > What each exit code and error code of the bohr CLI means, and what to do. Source: https://docs.bohr.io/cli/exit-codes/ | Code | Meaning | What to do | | --- | --- | --- | | **0** | Done | — | | **1** | Failed: the build, an upload, wrangler, a D1 migration, the instance, the network, or too many deploys in a few minutes | Read the message. Fix the build, or retry later for network errors and rate limits. | | **2** | Wrong usage: an unknown option, options that do not go together, no branch on a detached HEAD, an invalid `.bohr/deploy.json` or `.bohr/project.json` | Fix the command or the file. | | **3** | Not signed in, or the token was rejected (expired, revoked, malformed) | Run `bohr login`, or set `BOHR_TOKEN` to a valid token from **Account › Tokens**. | | **4** | Project not found for this token, no address for that branch, or the folder is not a bohr v2 project | Check `bohr projects`, the token's projects, or run it in the right folder. | | **5** | Not allowed: production without the owner role, a project in its owner's Cloudflare account without the owner role, a suspended project, a folder whose wrangler config names another Worker | Deploy a preview instead, push a branch for the project's workflow, or deploy production through the panel. | ## Error codes With `--json`, `error.code` says exactly what happened: | `error.code` | Exit | Meaning | | --- | --- | --- | | `usage` | 2 | Unknown command or option, or options that do not go together | | `no_project` | 2 | No `--project`, no link and no GitHub remote: *"Which project? … Pass --project <id> (see "bohr projects"), or run "bohr link"."* | | `no_branch` | 2 | Detached HEAD: pass `--branch` | | `invalid_deploy_config` | 2 | `.bohr/deploy.json` is not valid JSON or breaks [its rules](https://docs.bohr.io/deploys/deploy-json/) | | `invalid_link` | 2 | `.bohr/project.json` is invalid: run `bohr link` again | | `not_signed_in` | 3 | No token: run `bohr login` or set `BOHR_TOKEN` | | `invalid_token` | 3 | `BOHR_TOKEN` does not start with `bohr_` | | `login_denied`, `login_expired` | 3 | `bohr login` was denied, or its code expired | | `http_401` | 3 | The instance rejected the token (expired or revoked) | | `project_not_found`, `http_404` | 4 | No project for this repository or id that this token can reach | | `not_a_v2_project` | 4 | The folder is not a bohr v2 project (bohr v1 repositories included). Nothing was changed. | | `no_address` | 4 | `bohr open`: the branch was never deployed | | `own_account_not_allowed` | 5 | The project runs in its owner's Cloudflare account, and only its owners (admins of its organization) can deploy it from the CLI, previews included. Push a branch instead: its workflow deploys the production branch | | `worker_mismatch` | 5 | `--prod`: the folder's wrangler config names another Worker than the project's site. Nothing was deployed | | `own_account` | 5 | Only from CLI 4.0.0 and earlier, which could not deploy projects in their owner's Cloudflare account: update with `npx bohr@latest` | | `production_not_allowed` | 5 | Production needs the owner role | | `production_branch` | 5 | The production branch needs `--prod` | | `production_on_v1` | 5 | bohr v1 still serves the production address: deploy previews until the repository's workflow moves it | | `suspended`, `http_403` | 5 | The project is suspended, or the instance refused the action | | `cancelled` | 1 | You answered no to the production question | | `network_error` | 1 | The instance could not be reached | | `rate_limited` | 1 | Too many deploys of a project in your own Cloudflare account in the last minutes (HTTP 429): wait a few minutes | | `error`, `http_` | 1 | Anything else: the build failed, an upload failed, wrangler or a D1 migration failed, Cloudflare refused the deploy… | Messages are meant for people and may change; codes are stable. --- # CI usage > Run bohr deploy in GitHub Actions and other CI systems. Source: https://docs.bohr.io/cli/ci/ The CLI notices CI: without a terminal, or with `CI` set, it never asks questions and never opens a browser. ## GitHub Actions with OIDC (no secret) With `permissions: id-token: write` and no `BOHR_TOKEN`, the CLI signs in with the run's GitHub OIDC token and deploys the run's repository and pushed branch: ```yaml name: Deploy on: { push: { branches: ['**'] } } permissions: { contents: read, id-token: write } jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: { node-version: 22 } - id: bohr run: npx --yes bohr@latest deploy - run: echo "Deployed to ${{ steps.bohr.outputs.url }}" ``` - The CLI runs `.bohr/deploy.json`'s install and build first (add `--no-build` if an earlier step built the site). - In GitHub Actions the CLI writes the step outputs `url` and `deployment-id`, masks secrets with `::add-mask::` and groups its log. - `--prod`, `--branch` and `--project` need `BOHR_TOKEN`: with OIDC, the run's branch decides. - Workflows triggered from forks never get OIDC tokens. This is what [`bohr-io/deploy-action@v1`](https://docs.bohr.io/github-action/reference/) does too. ## With a token (any CI) Create a token in **Account › Tokens** limited to the project, store it as a CI secret, and pass it as `BOHR_TOKEN`: ```yaml - run: npx --yes bohr@latest deploy --project p3k9x0a7qd2m --branch "$GITHUB_REF_NAME" --json env: BOHR_TOKEN: ${{ secrets.BOHR_TOKEN }} ``` GitLab CI, with `BOHR_TOKEN` set as a masked CI/CD variable: ```yaml deploy: image: node:22 script: - npx --yes bohr@latest deploy --project p3k9x0a7qd2m --branch "$CI_COMMIT_REF_NAME" --json ``` Production from CI needs a token of a project **owner** and `--prod` (no question is asked without a terminal): ```sh BOHR_TOKEN=bohr_… npx --yes bohr@latest deploy --prod --json ``` ## Branch detection Without `--branch`, the CLI uses the checked-out branch; on a detached HEAD (common in CI) it falls back to `GITHUB_HEAD_REF`, then `GITHUB_REF`. Elsewhere, pass `--branch` explicitly. ## Pin a version `npx bohr@latest` always runs the latest release. To pin one, use `npx --yes bohr@4.1.0 deploy`, or add `bohr` to your `devDependencies`. :::note With `BOHR_TOKEN`, CI deploys reach projects in your own Cloudflare account too, when the token is an owner's ([how they deploy](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account)); in GitHub Actions the step outputs `url` and `deployment-id` are set as well. With OIDC (no `BOHR_TOKEN`), the CLI deploys only projects bohr hosts: projects in your own Cloudflare account deploy through their [deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/). ::: --- # Environment variables > The environment variables the bohr CLI reads. Source: https://docs.bohr.io/cli/environment-variables/ | Variable | Description | | --- | --- | | `BOHR_TOKEN` | A token from **Account › Tokens** (`bohr_…`). Wins over the login stored by `bohr login`. | | `BOHR_INSTANCE_URL` | The bohr instance, for self-hosted instances. Default `https://app.bohr.io`. `--instance` wins over it. | | `BOHR_CONFIG_DIR` | Where `credentials.json` lives. Default: `$XDG_CONFIG_HOME/bohr`, `~/.config/bohr` (Linux), `~/Library/Application Support/bohr` (macOS), `%APPDATA%\bohr` (Windows). | | `XDG_CONFIG_HOME` | Used for the config folder when `BOHR_CONFIG_DIR` is not set. | | `BOHR_DEBUG` | Any value prints error stack traces on stderr. | | `CI` | When set, the CLI never asks and never opens a browser. | Variables the CLI reads in GitHub Actions: | Variable | Use | | --- | --- | | `GITHUB_ACTIONS` | Turns on the Actions log format, outputs and masks | | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | Present with `id-token: write`: the CLI can sign in with OIDC | | `GITHUB_SHA` | The commit recorded with the deployment | | `GITHUB_HEAD_REF`, `GITHUB_REF` | The branch, on a detached HEAD | | `GITHUB_OUTPUT` | Where the step outputs `url` and `deployment-id` go | ## Precedence of the instance 1. `--instance ` 2. `BOHR_INSTANCE_URL` 3. `instanceUrl` in the folder's `.bohr/project.json` (`bohr deploy` and `bohr open`) 4. `https://app.bohr.io` --- # bohr-io/deploy-action reference > Inputs, outputs and modes of the bohr-io/deploy-action@v1 GitHub Action. Source: https://docs.bohr.io/github-action/reference/ `bohr-io/deploy-action@v1` deploys bohr projects from GitHub Actions. It is a thin wrapper that runs the latest [bohr CLI](https://docs.bohr.io/cli/install/) (`npx --yes bohr@latest action`) with the inputs below. It signs in to bohr with the run's **GitHub OIDC token**, so the job needs: ```yaml permissions: contents: read id-token: write ``` It has three modes: | Mode | Input | Used by | What it does | | --- | --- | --- | --- | | **Deploy** (default) | — | Projects bohr hosts | Uploads the built Worker and static assets to bohr's hosting, for the pushed branch | | **Credentials** | `credentials: "true"` | Projects in your own Cloudflare account (in the workflow bohr writes) | Gets the Cloudflare credentials and build variables for `wrangler deploy`; deploys nothing | | **Build plan** | `build-plan: "true"` | Projects migrated from bohr v1 that keep their v1 workflow | Gets the project's build plan; deploys nothing | You rarely add the action yourself: bohr writes it into [`bohr-deploy.yml`](https://docs.bohr.io/deploys/deploy-workflow/) for projects in your account, and migrated projects run it through their v1 workflow. ## Inputs | Input | Default | Description | | --- | --- | --- | | `instance-url` | `https://app.bohr.io` | Base URL of the bohr instance that manages the project. Must use `https`. | | `working-directory` | `.` | Folder that holds the project's `wrangler.jsonc` (or `wrangler.json`), relative to the repository root. Deploy mode. | | `credentials` | `false` | `"true"`: only get the deploy credentials and build variables of a project in its owner's account. | | `build-plan` | `false` | `"true"`: only prepare the build of a project migrated from bohr v1. | | `phase` | `prepare` | With `build-plan: "true"`: the step to run. Only `prepare` exists. | `credentials` and `build-plan` cannot both be `"true"`. ## Outputs | Output | Mode | Description | | --- | --- | --- | | `url` | Deploy | URL where the deployed branch is served | | `deployment-id` | Deploy | Identifier of the deployment on the bohr instance | | `cloudflare-api-token` | Credentials | The Cloudflare API token to deploy with (masked) | | `cloudflare-account-id` | Credentials | The Cloudflare account to deploy to (masked) | | `found` | Build plan | `"true"` when bohr has a build plan for this repository, else `"false"` | | `node-version` | Build plan | The Node.js version for `actions/setup-node` | | `install` | Build plan | The install command (empty: none) | | `build` | Build plan | The build command (empty: none) | | `live` | Build plan | `"true"` when the project's production address already moved to bohr v2 | In deploy mode the job summary shows the URL and how many files were found and uploaded. ## Deploy mode Run your build before the action: it deploys what the build wrote, and does not install dependencies or build. Every pushed branch is deployed to its own address; the production branch gets the project's main address. The action reads `wrangler.json` or `wrangler.jsonc` in `working-directory`, bundles `main` with `wrangler deploy --dry-run` (add `wrangler` to your `devDependencies` to pin it), and uploads only new or changed static files. Hosted projects support static assets, `vars` and secrets: see [Wrangler config](https://docs.bohr.io/deploys/wrangler-config/#projects-hosted-by-bohr). ## Credentials mode With `credentials: "true"` the action asks bohr for the credentials (`POST /api/deploy/credentials` with the OIDC token), masks every value, and then: - exports the project's [build variables](https://docs.bohr.io/projects/variables/#build-variables) (production branch) to the environment of the next steps, so install and build see them; - sets the Cloudflare token and account as the step's outputs, so only the step that maps them (the deploy) gets them. bohr answers only runs of `.github/workflows/bohr-deploy.yml` on a `push` (or `workflow_dispatch`) to the project's production branch, for a project in its owner's Cloudflare account. It hands out the owner's own token (yours, or your organization's), never bohr's. ## Build plan mode For repositories migrated from bohr v1 that keep their workflow (`.github/workflows/bohr.yml` running `bohr-io/action@main`). bohr v1's action calls this one twice: to prepare (`build-plan: "true"`), then to deploy. Nothing is written into your repository. See [Migrating from bohr v1](https://docs.bohr.io/migrating-from-v1/what-changes/). ## Errors | Error | Fix | | --- | --- | | `Could not get a GitHub OIDC token` | Add `permissions: id-token: write` to the workflow or the job. Workflows triggered from forks never get OIDC tokens. | | `No bohr.io Cloud managed project matches this repository and branch` | Create or migrate the project in the bohr panel, and deploy on branch pushes (not tags or pull requests). | | `could not give the deploy credentials (HTTP 403)` | Credentials mode: the message says why. Only `bohr-deploy.yml` on a push to the production branch of a project in its owner's account gets them. | | `Found wrangler.toml, but this action reads wrangler.jsonc` | Convert the config to `wrangler.jsonc`. | | `… is not supported` | Remove the binding; hosted projects support static assets, vars and secrets. | | `The assets directory … does not exist` | Run the build before the action, or fix `assets.directory`. | | `Asset too large: …` | Files over 25 MiB cannot be deployed: remove it or list it in `.assetsignore`. | --- # Examples > Workflows that use bohr-io/deploy-action@v1. Source: https://docs.bohr.io/github-action/examples/ ## Deploy every branch of a project bohr hosts ```yaml name: Deploy on: { push: { branches: ['**'] } } permissions: { contents: read, id-token: write } jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: { node-version: 22 } - run: npm ci && npm run build # the project's own build - id: deploy uses: bohr-io/deploy-action@v1 - run: echo "Deployed to ${{ steps.deploy.outputs.url }}" ``` ## A project in a subfolder ```yaml - run: npm ci && npm run build working-directory: apps/web - uses: bohr-io/deploy-action@v1 with: working-directory: apps/web ``` ## A self-hosted instance ```yaml - uses: bohr-io/deploy-action@v1 with: instance-url: https://bohr.example.com ``` ## Credentials for wrangler (projects in your own account) This is the part of the workflow bohr writes for you ([full workflow](https://docs.bohr.io/deploys/deploy-workflow/)). The file must be `.github/workflows/bohr-deploy.yml`, running on pushes to the production branch: ```yaml permissions: { contents: read, id-token: write } jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 - name: bohr credentials id: bohr-credentials uses: bohr-io/deploy-action@v1 with: credentials: "true" - run: npm ci && npm run build # sees the build variables - run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ steps.bohr-credentials.outputs.cloudflare-api-token }} CLOUDFLARE_ACCOUNT_ID: ${{ steps.bohr-credentials.outputs.cloudflare-account-id }} ``` ## The CLI as a plain step The same deploy without the action: ```yaml - id: bohr run: npx --yes bohr@latest deploy - run: echo "Deployed to ${{ steps.bohr.outputs.url }}" ``` See [CI usage](https://docs.bohr.io/cli/ci/). --- # Deploying from AI coding agents > How Claude Code, Cursor, Codex and other coding agents deploy bohr projects with the CLI. Source: https://docs.bohr.io/ai-agents/overview/ AI coding agents (Claude Code, Cursor, Codex, Windsurf, Aider, your own scripts) can deploy bohr projects with the [bohr CLI](https://docs.bohr.io/cli/install/): build, upload and get a preview URL to show you, without a browser and without your password. This section is written for you **and** for the agent. Point your agent at it, or give it the ready-made instructions: - [AGENTS.md / CLAUDE.md snippet](https://docs.bohr.io/ai-agents/agents-md/): paste into your repository's instructions file. - [SKILL.md](https://docs.bohr.io/ai-agents/skill/): a skill file the agent loads when it needs to deploy. ## The short version ```sh # 1. You: create a token in the panel (Account › Tokens), limited to the project, and give it to the agent export BOHR_TOKEN=bohr_… # 2. The agent: find the project once npx --yes bohr@latest projects --json # 3. The agent: deploy a preview after each change, and read "url" from the JSON npx --yes bohr@latest deploy --project p3k9x0a7qd2m --branch agent/new-header --json ``` ## How it fits with bohr's own agent bohr already has an agent that changes your site from a chat, with a preview and an approval. An external coding agent is for when you work in your editor or terminal instead: it changes the code locally and uses `bohr deploy` to publish previews of its work. | | bohr's agent | Your coding agent + CLI | | --- | --- | --- | | Where it runs | The sandbox in your Cloudflare account | Your machine, or the agent's own sandbox | | Preview | The `homolog` branch, approved in the panel | Any branch name, with `bohr deploy` | | Production | Approve in the panel or on Telegram | `bohr deploy --prod` (owners), only when you ask | ## Read on 1. [Set up a token](https://docs.bohr.io/ai-agents/setup/): scoped, non-interactive. 2. [The deploy loop](https://docs.bohr.io/ai-agents/deploy-loop/): `--json`, exit codes, and preview vs `--prod` etiquette. 3. [AGENTS.md snippet](https://docs.bohr.io/ai-agents/agents-md/) and [SKILL.md](https://docs.bohr.io/ai-agents/skill/). :::note[Projects in your own Cloudflare account] `bohr deploy` publishes projects in your own Cloudflare account too, but only with a token of the project's owner (previews included), since wrangler deploys them with the owner's Cloudflare connection. With anyone else's token it exits with code 5 (`own_account_not_allowed`); the agent should then commit to a branch and open a pull request, and the merge deploys through the project's workflow. Their previews use production's D1 databases. See [the deploy loop](https://docs.bohr.io/ai-agents/deploy-loop/#projects-in-your-own-cloudflare-account). ::: --- # Set up a token for an agent > Create a scoped bohr token and give it to an AI agent without exposing it. Source: https://docs.bohr.io/ai-agents/setup/ ## 1. Create a scoped token In the panel, open **Account › Tokens** and fill in **New token**: | Field | Recommendation | | --- | --- | | **Name** | Something you will recognize later, like `claude code - shop` | | **Projects** | **Only these projects**, and pick only the ones the agent works on. Avoid **All my projects, including the ones I join later**. | | **Expires** | **In 30 days**, **In 90 days** or **In 365 days**. Prefer a short one; create a new token when it expires. | Click **Create token** and copy it: the panel shows it once (*"Anyone who has it can deploy as you."*). A token acts as **you**, on the projects you are a member of, limited to the ones it lists. It can deploy previews; it can deploy production only on projects you own, and projects in their owner's Cloudflare account only when you own them (previews included). It never carries an instance administrator role. ## 2. Give it to the agent Set it as the `BOHR_TOKEN` environment variable where the agent runs commands. Never paste it into the chat or a file in the repository. ```sh # a shell you start the agent from export BOHR_TOKEN=bohr_… claude ``` - **Claude Code**: export it in the shell before starting `claude`, or add it to the `env` of your user settings (`~/.claude/settings.json`), never to the repository's `.claude/settings.json`. - **Cursor, Windsurf**: set it in the environment the editor's terminal inherits. - **Codex and cloud agents**: add it as a secret of the agent's environment. - **CI**: a masked CI secret (see [CI usage](https://docs.bohr.io/cli/ci/)). Check it works: ```sh npx --yes bohr@latest whoami --json ``` ## 3. Non-interactive by default The CLI never asks questions and never opens a browser when there is no terminal, when `CI` is set, or with `--json`. Agents should always pass `--json`. Agents should not run `bohr login`: it needs a person to approve it in a browser. ## Revoking **Account › Tokens › Revoke** stops the token at once. Deploys it already published stay online. Each token shows when it was **last used**. --- # The deploy loop > How an AI agent should call the CLI, parse its JSON, react to exit codes, and when (not) to deploy production. Source: https://docs.bohr.io/ai-agents/deploy-loop/ ## 1. Find the project once ```sh npx --yes bohr@latest projects --json ``` Match `repository` (`owner/name`) against the repository's GitHub remote and keep the project's `id`. Check: - `mode`: `managed` means bohr hosts it; `byo` means it runs in its owner's Cloudflare account (see [below](#projects-in-your-own-cloudflare-account)). `bohr deploy` publishes both. - `role`: `owner` is needed for production, and for any deploy of a `byo` project. If the repository has `.bohr/project.json` (from `bohr link`), `--project` can be left out. ## 2. Deploy a preview after each change ```sh npx --yes bohr@latest deploy --project p3k9x0a7qd2m --branch agent/fix-contact-form --json ``` - Pick a branch name that describes the change; it does not have to exist in git. Each branch gets its own address, so previews of different changes do not overwrite each other. - The CLI runs `.bohr/deploy.json`'s install and build first. Pass `--no-build` only when the agent already built the site in the same environment. - Read the result from stdout, and show `url` to the person. ```json { "ok": true, "url": "https://agent-fix-contact-form-shop-acme.bohr.io", "branch": "agent/fix-contact-form", "production": false, "...": "…" } ``` Full schema: [JSON output](https://docs.bohr.io/cli/json-output/#bohr-deploy). ## 3. React to the exit code | Exit | The agent should | | --- | --- | | 0 | Report `url`. | | 1 | Read `error.message` (and stderr). Fix a build error and retry **once**. For `network_error`, wait and retry once. | | 2 | Fix the command or the file the message names. `no_branch`: pass `--branch`. | | 3 | **Stop** and ask the person for a valid `BOHR_TOKEN`. Do not run `bohr login`. | | 4 | Re-check `projects --json`. `not_a_v2_project`: stop and tell the person; the folder is not a bohr v2 project. | | 5 | **Do not retry.** Explain why (`own_account_not_allowed`, `production_not_allowed`, `production_on_v1`, `worker_mismatch`, `suspended`) and deploy a preview instead when that makes sense. | All codes: [Exit codes and errors](https://docs.bohr.io/cli/exit-codes/). ## Preview vs production etiquette - **Previews are the default** and safe: they never touch production. Deploy one whenever you want the person to see a change. - **Production only on an explicit request** in the current conversation, such as *"deploy it to production"*. A previous approval, a passing build or a merged branch is not a request. - Before `--prod`, say what will be published. `--prod` from an agent runs without the confirmation question, so the agent is the last check. - Never deploy production to fix a production problem without asking; deploy a preview of the fix first. - Do not run `bohr deploy` in a loop. One deploy per meaningful change. ## Projects in your own Cloudflare account `bohr deploy` deploys projects in their owner's Cloudflare account (`mode: "byo"`) with wrangler, using the Cloudflare token their owner connected to bohr ([details](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account)). The same loop works, with these differences: - **Only an owner's token** can deploy them, previews included. With anyone else's, the CLI exits with code 5 and `error.code: "own_account_not_allowed"` before building anything. Then the agent should commit its change to a new branch, push it, open a pull request to the production branch and let a person review and merge it; the project's workflow deploys the production branch. Never push to the production branch directly. The person can also ask bohr's own agent, in the panel or on Telegram, for the change: it comes with a preview to approve. - A preview is a new Worker version under an alias of the branch; it never changes production. If `url` is `null`, the Worker has no preview URLs (`"preview_urls": true` in its wrangler config): tell the person. - **Previews use production's D1 databases** and apply no migrations. Ask before deploying a preview that writes data or depends on a schema change. - `--prod` applies the D1 migrations, then deploys. `worker_mismatch` (exit code 5) means the folder's wrangler config names another Worker: nothing was deployed. - `error.code: "own_account"` comes only from CLI 4.0.0 and earlier: run `npx --yes bohr@latest`. --- # AGENTS.md / CLAUDE.md snippet > Copy-paste instructions that teach a coding agent to deploy your project with the bohr CLI. Source: https://docs.bohr.io/ai-agents/agents-md/ Paste this section into your repository's instructions file: `AGENTS.md` (Codex, Cursor and many others), `CLAUDE.md` (Claude Code), `.cursor/rules`, or whatever your agent reads. It tells the agent how to deploy previews, when not to deploy production, and what to do on each exit code. **Download:** [AGENTS.md](https://docs.bohr.io/downloads/AGENTS.md) (plain Markdown). To append it to your `AGENTS.md` from a terminal (it is added only if the download really is the snippet): ```sh tmp=$(mktemp) if curl -fsSL https://docs.bohr.io/downloads/AGENTS.md -o "$tmp" && grep -q 'npx --yes bohr@latest' "$tmp" && ! grep -qi '> AGENTS.md && cat "$tmp" >> AGENTS.md && echo "Added to AGENTS.md" else echo "The download is not the bohr snippet: AGENTS.md was not changed." >&2 fi rm -f "$tmp" ``` ````markdown ## Deploying with bohr This project deploys with [bohr](https://docs.bohr.io). Use the bohr CLI; never deploy with other tools. - Authentication: the `BOHR_TOKEN` environment variable (a token from the bohr panel, Account › Tokens). Never print it, log it, commit it or pass it on the command line. - Always run the CLI non-interactively with `--json`: `npx --yes bohr@latest --json`. stdout is exactly one JSON object; logs go to stderr. - Find the project once: `npx --yes bohr@latest projects --json`. Use the `id` of the project whose `repository` matches this repo. `mode: "managed"`: bohr hosts it. `mode: "byo"`: it runs in its owner's Cloudflare account, and only an owner's token can deploy it (previews too). - Deploy a **preview** after each change you want to show: `npx --yes bohr@latest deploy --project --branch --json` Read `url` from the JSON and give it to the user. Use a branch name that describes the change (for example `agent/fix-contact-form`). - **Never deploy production** (`--prod`) unless the user explicitly asked for a production deploy in this conversation. Production needs the owner role. - Never commit `.env` files or secrets. bohr never publishes `.git` or `.env*` files. - Exit codes: 0 done; 1 failed (read `error.message`, fix the build, retry once); 2 wrong usage (fix the command); 3 not signed in or token rejected (stop and ask the user for a valid `BOHR_TOKEN`); 4 project not found or not a bohr v2 project (check `projects --json`, do not guess); 5 not allowed (do not retry; tell the user: production without the owner role, a project in its owner's Cloudflare account without the owner role, a suspended project, or a wrangler config for another Worker). - If `error.code` is `own_account_not_allowed`, this token cannot deploy the project: commit to a branch, open a pull request to the production branch and let a person merge it. Do not push to the production branch yourself. If it is `own_account`, the CLI is outdated: run `npx --yes bohr@latest`. - For `mode: "byo"` projects, previews use the production D1 databases and apply no migrations: ask the user before deploying a preview that writes data or needs a schema change. If `url` is `null`, the Worker has no preview URLs (`"preview_urls": true` in its wrangler config). - Build settings live in `.bohr/deploy.json`; the preview's dev server in `.bohr/preview.json`. Do not edit `.github/workflows/bohr-deploy.yml`: bohr generates it. ```` ## Adapting it - If you always deploy the same project, replace the `projects --json` step with its id, or run `bohr link` and commit `.bohr/project.json`. - Keep the production rule unless you really want the agent to publish without asking, and keep the token limited to the project either way. - Keep the `own_account_not_allowed` and D1 rules for projects in your own Cloudflare account: only an owner's token deploys them, and their previews use production's D1 databases ([details](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account)). --- # SKILL.md for agents > A skill file in the Claude Code skill format with everything an AI agent needs to deploy bohr projects. Source: https://docs.bohr.io/ai-agents/skill/ A **skill** is an instruction file an agent loads only when it needs it. This one follows the Claude Code skill format (a folder with a `SKILL.md` that starts with a `name` and a `description`); other agents can read it as plain instructions. **Download:** [SKILL.md](https://docs.bohr.io/downloads/bohr-deploy/SKILL.md). ## Install it For Claude Code, put it in a `bohr-deploy` folder of your skills. The command downloads it to a temporary file and only installs it if it really is the bohr skill (it starts with `---` and `name: bohr-deploy`), so a failed or wrong download never replaces a skill you have: ```sh # for one repository (commit it so your team gets it too); for every project on your computer, # use dir=~/.claude/skills/bohr-deploy instead dir=.claude/skills/bohr-deploy tmp=$(mktemp) if curl -fsSL https://docs.bohr.io/downloads/bohr-deploy/SKILL.md -o "$tmp" && [ "$(head -n 2 "$tmp")" = "$(printf '%s\n%s' '---' 'name: bohr-deploy')" ]; then mkdir -p "$dir" && mv "$tmp" "$dir/SKILL.md" && echo "Installed $dir/SKILL.md" else rm -f "$tmp"; echo "The download is not the bohr skill: nothing was installed." >&2 fi ``` Other agents: save it anywhere and point to it from your instructions file, or paste it into the agent's instructions. ## The file ````markdown --- name: bohr-deploy description: Deploy a bohr project (bohr.io) with the bohr CLI. Use when the user asks to deploy, publish, preview or ship a site that deploys with bohr, when the repository has a .bohr/ folder (.bohr/deploy.json, .bohr/project.json), or when BOHR_TOKEN is set. --- # Deploying bohr projects bohr (https://docs.bohr.io) deploys GitHub repositories as Cloudflare Workers. The `bohr` CLI (npm package `bohr`, Node.js 20+) deploys a project from this machine: previews of any branch, and production for project owners. ## Rules 1. Use only the bohr CLI to deploy, always non-interactively and with `--json`: `npx --yes bohr@latest --json` stdout is exactly one JSON object. Logs and build output go to stderr. 2. Authenticate with the `BOHR_TOKEN` environment variable. Never print, log, commit or echo the token, and never put it in a command line. If it is missing, ask the user to create one in the bohr panel (Account › Tokens), limited to this project, and to set it as `BOHR_TOKEN`. 3. Deploy **previews** by default. Deploy **production** (`--prod`) only when the user explicitly asked for a production deploy in this conversation. 4. Never commit secrets or `.env` files. Put secrets in the bohr panel (Settings › Environment variables), not in `wrangler.jsonc`. 5. Do not edit `.github/workflows/bohr-deploy.yml` (bohr generates it from `.bohr/deploy.json`). ## Steps ### 1. Find the project (once per session) ```sh npx --yes bohr@latest projects --json ``` Pick the project whose `repository` (`owner/name`) matches this repository's GitHub remote. Remember its `id`. - `mode: "managed"`: bohr hosts it; `bohr deploy` publishes it. - `mode: "byo"`: it runs in its owner's Cloudflare account; `bohr deploy` publishes it with wrangler, for owners only (see "Projects in the owner's Cloudflare account"). - `role: "owner"` is required for production, and for any deploy of a `byo` project. Alternatively, if the repository has `.bohr/project.json`, the CLI already knows the project and `--project` can be left out. ### 2. Deploy a preview after each change ```sh npx --yes bohr@latest deploy --project --branch --json ``` - Use a descriptive branch name, for example `agent/`. It does not need to exist in git. - The CLI runs the install and build from `.bohr/deploy.json` first. Add `--no-build` only if you already built the site in this environment. - On success, read `url` and give it to the user. Also useful: `deploymentId`, `files`, `uploadedFiles`. Example success: ```json {"ok":true,"url":"https://agent-fix-form-shop-acme.bohr.io","deploymentId":"d_…","project":{"id":"p3k9x0a7qd2m","name":"shop"},"branch":"agent/fix-form","production":false,"files":42,"bytes":1363148,"uploadedFiles":3,"uploadedBytes":20480,"modules":1} ``` ### 3. Production (only when asked) ```sh npx --yes bohr@latest deploy --project --prod --json ``` ## Handling the result On failure stdout is `{"ok":false,"error":{"code","message","exitCode"}}` and the process exits with `exitCode`. Branch on the exit code and `error.code`: | Exit | Meaning | Do | | --- | --- | --- | | 0 | Done | Report `url`. | | 1 | Failed (build, upload, wrangler, D1 migration, network, instance, `rate_limited`) | Read `error.message` and the stderr log. Fix a build error and retry once. For `network_error`, retry once after a short wait. For `rate_limited`, stop and tell the user to wait a few minutes. | | 2 | Wrong usage (`usage`, `no_project`, `no_branch`, `invalid_deploy_config`, `invalid_link`) | Fix the command or the file named in the message. For `no_branch`, pass `--branch`. | | 3 | Not signed in or token rejected (`not_signed_in`, `invalid_token`, `http_401`) | Stop. Ask the user for a valid `BOHR_TOKEN`. Do not try `bohr login` (it needs a person and a browser). | | 4 | Not found (`project_not_found`, `not_a_v2_project`, `no_address`) | Re-check `projects --json` and the token's projects. `not_a_v2_project` means this folder is not a bohr v2 project (maybe a bohr v1 repository): stop and tell the user. | | 5 | Not allowed (`own_account_not_allowed`, `production_not_allowed`, `production_on_v1`, `worker_mismatch`, `suspended`, `http_403`, `own_account`) | Do not retry. Tell the user why. For `production_not_allowed` or `production_on_v1`, deploy a preview instead. For `own_account_not_allowed`, see below. `own_account` comes only from an outdated CLI: run `npx --yes bohr@latest`. | ## Projects in the owner's Cloudflare account (`mode: "byo"`) `bohr deploy` deploys them with wrangler, using the Cloudflare token their owner connected to bohr. The result has `"mode": "byo"`, plus `versionId`, `previewUrl`, `workerName` and `commitSha` (the upload counts are `null`). - Only the project's owners (admins of its organization) can deploy them, previews included. Anyone else gets exit code 5 with `own_account_not_allowed`: do not push to the production branch yourself; commit your change to a branch, push it, open a pull request to the production branch, and let a person review and merge it. The project's own bohr agent (in the bohr panel or on Telegram) can also make the change with a preview. - A preview uploads a new Worker version and never changes production. If `url` is `null`, the Worker has no preview URLs: tell the user to set `"preview_urls": true` in its wrangler config. - Previews use the **production D1 databases** and apply no D1 migrations. Ask the user before deploying a preview that writes data or depends on a schema change. - `--prod` applies the D1 migrations, then deploys. `worker_mismatch` means the folder's wrangler config names another Worker than the project's: nothing was deployed; tell the user. ## Useful files - `.bohr/deploy.json`: how production builds (`directory`, `node`, `packageManager`, `install`, `build`, `deploy`, `secrets`). - `.bohr/preview.json`: how the preview's dev server runs (`install`, `command`, `port`, `env`). - `wrangler.jsonc`: the Worker. Projects bohr hosts support static assets, `vars` and secrets only; other bindings make the deploy fail. - Static files over 25 MiB cannot be deployed. `.git` and `.env*` files are never published. Docs: https://docs.bohr.io/ai-agents/overview/ (Markdown: https://docs.bohr.io/llms.txt) ```` --- # Organizations > Work as a team in a bohr organization, which mirrors a GitHub organization. Source: https://docs.bohr.io/organizations/overview/ A bohr **organization** is a GitHub organization with bohr's GitHub App installed. Its members come from GitHub, and it has its own subscription (on bohr.io Cloud), Cloudflare account and agent settings, separate from each person's. ## Create an organization You need a GitHub organization where you are an **owner**. bohr cannot create GitHub organizations: create one on GitHub first if needed (**Create an organization on GitHub**). 1. Open the context menu at the top of the panel and choose **New organization**. 2. Click **Install the GitHub app on an organization**. 3. On GitHub, pick the organization and the repositories bohr can access, and confirm. 4. GitHub brings you back to the organization's page in bohr. bohr imports the organization's members with the matching role: GitHub owners become **admins**, everyone else **members**. If the App was installed outside bohr by someone who already uses bohr, the organization is created too. ## Set it up The organization's page shows **Set up the organization** to its admins, with three steps (like your own account): | Step | | | --- | --- | | **Subscription** | bohr.io Cloud only: the organization's own subscription, paid by an admin. Members don't need one. | | **Cloudflare** | The Cloudflare account where every project of the organization runs (Workers Paid). Same token as [for yourself](https://docs.bohr.io/getting-started/connect-cloudflare/). | | **Agent** | The provider and key the agent uses in every project of the organization. Same options as [your own](https://docs.bohr.io/agent/providers/), including a Claude subscription. | Until it is set up, members see *"An admin of the organization still needs to finish its setup (subscription and Cloudflare account) before projects can be created."* ## Switch between personal and organization The context menu at the top of the panel (**Work in**) lists **Personal** and each of your organizations. The project list, **New project** and settings follow the context you pick. - Projects created in an organization use the organization's Cloudflare account and agent, whoever creates them. - On your personal context, a list reminds you of organizations whose projects need no setup in your account. ## Organization pages | Page | | | --- | --- | | **Projects** | The organization's projects, and **New project** (one of the organization's repositories) | | **Members** | Members, roles and sync with GitHub. See [Members and roles](https://docs.bohr.io/organizations/members/). | | **Settings** | General, Cloudflare, agent, subscription and the danger zone. See [Settings and billing](https://docs.bohr.io/organizations/settings-and-billing/). | --- # Organization members and roles > How organization members mirror GitHub, their roles, and how to change or remove access. Source: https://docs.bohr.io/organizations/members/ **GitHub is the source of the members.** bohr mirrors the members of the GitHub organization: owners on GitHub become **admins** in bohr, everyone else **members**. | Role | Can | | --- | --- | | **Admin** | Everything a project owner can on every project of the organization (settings, domains, variables, deleting); the organization's settings, subscription, Cloudflare account and agent; members' roles | | **Member** | Work on every project of the organization: ask for changes, approve, discard, roll back | Members need no subscription, Cloudflare account or model key of their own. ## Keeping in sync - People added to or removed from the GitHub organization follow automatically (GitHub notifies bohr). - **Sync with GitHub** on the **Members** page syncs by hand: *"Members synced: N added, N removed, N updated."* - People without a bohr account yet show as **Pending** (*"Joins when they sign in to bohr with GitHub."*). They become active when they sign in with that GitHub account, or link it to their bohr account. ## Changing access in bohr Admins can change things in bohr without touching GitHub. The **Source** column shows **Changed in bohr** for those. - **Make admin** / **Make member**: the role lasts until that person's role changes on GitHub. - **Remove**: the person loses access to the organization's projects in bohr. They stay in the GitHub organization, and syncing doesn't bring the access back. - The organization needs at least one admin: *"The organization needs at least one admin. Make someone else an admin first."* ## When members are not imported If the GitHub App lacks the **Members: read** permission on the organization, only the person who installed it is added, and the page says *"The members were not imported"*. Grant the permission on GitHub (the organization's App settings ask you to accept it), then **Sync with GitHub**. ## Project members outside the organization Project [invites](https://docs.bohr.io/projects/members/) also work in organization projects, for people outside the GitHub organization such as a client who reviews the preview. --- # Organization settings and billing > An organization's Cloudflare account, agent, subscription, and how to delete it. Source: https://docs.bohr.io/organizations/settings-and-billing/ **Settings** of an organization. Only its admins change them (*"Only the organization's admins change its settings."*). ## General The **Name** and picture come from the GitHub organization; the page links to it on **GitHub**. ## Cloudflare The organization's projects run in this Cloudflare account. **Connect an account** with a token, as [for yourself](https://docs.bohr.io/getting-started/connect-cloudflare/), or **Connect another account**. An account cannot be disconnected while projects of the organization run there. ## Agent The agent's provider, key, model and effort in every project of the organization. The same choices as [personal settings](https://docs.bohr.io/agent/providers/): an Anthropic API key, a compatible provider, or a Claude subscription. Saving applies to every ready project of the organization; removing turns the agent off in all of them (GitHub deploys keep working). ## Subscription (bohr.io Cloud) Each organization has its **own** subscription, US$20/month, paid by one of its admins with an organization customer on Stripe that never mixes with the admin's personal subscription. Members don't need a subscription. Admins subscribe from the organization's setup, and manage it (card, invoices, cancellation) with **Manage on Stripe**. See [Subscription](https://docs.bohr.io/billing/subscription/). ## When the GitHub App is uninstalled If someone uninstalls bohr's App from the GitHub organization, the page says *"The GitHub app was uninstalled"*: the projects stay online, but you can't create new ones or sync the members. **Install the app again** to fix it. ## Danger zone: delete the organization **Delete organization** removes its projects from bohr, and everything bohr created for them. Type the organization's slug to confirm. - The organization's projects are deleted (agents, previews, bots and bohr routes). - The organization's subscription is canceled, and its Cloudflare accounts and agent are disconnected. - The bohr app is uninstalled from the GitHub organization. **The repositories aren't changed.** - The sites published in the organization's Cloudflare account **stay online**, unless you check **Also delete the sites published in the organization's Cloudflare account**. --- # Your account and connections > Profile, language, ways to sign in, and disconnecting GitHub, Telegram, Cloudflare and the agent. Source: https://docs.bohr.io/account/connections/ **Account** (sidebar) gathers your profile and everything connected to your bohr account. ## Profile and language - **Profile**: your **Name** and **Email**. - **Language**: the **Interface language** (Português, English, Español). The choice is saved in that browser. - **Sign out** ends the session on this browser. ## Connected accounts ### Ways to sign in How you sign in to bohr: **GitHub**, **Email** and **Telegram**. Your account needs at least one. - **Connect GitHub sign-in** links a GitHub account. - **Add email**: type the address, **Send code**, type the 6-digit code and **Add email**. *"This email already belongs to another bohr account."* means it is taken. - **Link Telegram** (when the instance has a platform bot): opens the bot; tap **Start**. Then the project bots recognize you. - **Remove** a way to sign in. The last one cannot be removed: *"This is the only way to sign in to your account. Add another one before removing it."* ### GitHub App The GitHub accounts where bohr's app is installed, with how many projects use each. **Disconnect** uninstalls the app from that account and bohr stops accessing its repositories. Projects that still use it must be deleted first; repositories are not changed. ### Cloudflare The Cloudflare accounts your projects are published to. **Disconnect** deletes the API token bohr kept for that account; sites published there stay online. Projects that still use the account must be deleted first. Also delete the token itself in the Cloudflare dashboard (**My Profile → API Tokens**). ### Telegram **Disconnect** makes the bohr bot stop recognizing you. Project bots stay in Telegram. ### Agent Your model **Provider**, **Model** (or *bohr's default*) and the end of your **Key**. **Configure** opens [Model providers](https://docs.bohr.io/agent/providers/). **Disconnect** deletes the provider key and turns the agent off in all your projects; they keep publishing what reaches GitHub. ## Also on this page - **Subscription**: whether your bohr.io Cloud subscription is active, and **See the subscription**. See [Subscription](https://docs.bohr.io/billing/subscription/). - **Agent**: **Set up the agent**. - **Tokens and the CLI**: **Manage tokens**. See [Tokens](https://docs.bohr.io/account/tokens/). - **Danger zone**: [delete your account](https://docs.bohr.io/account/delete-account/). --- # Tokens > Create, scope and revoke tokens for the bohr CLI, scripts and AI agents. Source: https://docs.bohr.io/account/tokens/ A token lets a script or an AI agent use bohr as you, with the [bohr CLI](https://docs.bohr.io/cli/install/): list your projects and deploy previews (production too, on projects you own; projects in their owner's Cloudflare account only when you own them). Give it to them as the `BOHR_TOKEN` environment variable. Manage them in **Account › Tokens** (**Manage tokens**). ## Create a token Under **New token**: | Field | | | --- | --- | | **Name** | One you will recognize, for example "deploy agent" or "CI of the store". | | **Projects** | **All my projects, including the ones I join later**, or **Only these projects** (pick at least one). | | **Expires** | **In 30 days**, **In 90 days**, **In 365 days** or **Never**. | Click **Create token**. The panel shows the token **once**, with how to use it: ```sh BOHR_TOKEN=bohr_… npx bohr deploy --json ``` Copy it now and keep it somewhere safe: bohr does not show it again, and anyone who has it can deploy as you. bohr stores only a hash of it. ## What a token can do - It acts as **you**, on the projects you are a member of (an organization's projects through the organization), limited to the projects it lists. - Previews on any of those projects; production only on projects you own (or whose organization you administer). - It never carries the instance's administrator role, even if you have it. - You can have up to 50 tokens. ## Your tokens **Your tokens** lists each token with its projects (**All your projects** or **Projects: …**), when it was **Created**, when it was **last used** (or *never used*), and when it expires (or *never expires*). Expired tokens show **Expired**. Each computer where you ran `bohr login` has a token of its own, marked **CLI** and named "CLI on <computer>". `bohr logout` revokes it. ## Revoke **Revoke** and confirm: scripts, agents and computers using the token stop working at once. Deploys already published stay online. --- # Delete your account > Remove your bohr account and everything bohr created for it. Source: https://docs.bohr.io/account/delete-account/ The **Danger zone** at the bottom of **Account** deletes your account. It cannot be undone. 1. Click **Delete account**. 2. Read what goes and what stays. 3. Optional: check **Also delete the sites published in my Cloudflare account**. 4. Type `DELETE` to confirm (`EXCLUIR` in Portuguese). ## What goes - All your projects: the agent Workers and the bohr hosting. - The GitHub App is uninstalled from your accounts. - The Cloudflare connections are removed. - The Telegram link is removed. - The agent settings are deleted. - The bohr.io Cloud subscription is canceled. ## What stays - **GitHub repositories are never touched.** - **Project bots stay in Telegram.** Delete them with BotFather if you want. - **The sites published in your Cloudflare account stay online**, unless you checked the option, which removes the sites' production Workers from the connected Cloudflare accounts. ## Your organizations - An organization where you are the **only active member** is deleted with your account. - In an organization with other members, the projects you created, the Cloudflare connections you made and the subscription you pay pass to another active member (an admin when there is one). - If you are the **last admin** of an organization with other members, bohr refuses: *"You are the last admin of … Make another member an admin first, or delete the organization."* On a self-hosted instance, its only administrator cannot delete their account while other users exist (*"You are the only admin. Make someone else an admin first."*). When something could not be removed, the sign-in page shows *"A few things were left behind and need to be removed by hand"* with the list. --- # Subscription > The bohr.io Cloud subscription, what it includes, and how to manage it. Source: https://docs.bohr.io/billing/subscription/ **bohr.io Cloud costs US$20 per month** per personal account or organization. bohr runs and maintains the instance for you (the panel, the Telegram bot, the credential vault), and your projects run in your own Cloudflare account. Stripe handles the billing. Self-hosted instances have no subscription: *"This instance does not charge a subscription."* ## What is included - The bohr instance at [app.bohr.io](https://app.bohr.io), with every feature: the agent, previews, approvals, versions, members, organizations, variables, password protection, domains, the CLI and the GitHub Action. - The Telegram platform bot and email sign-in. ## What is not included Infrastructure is paid directly to the providers: - **Cloudflare**: the Workers Paid plan (US$5/month) on the account your projects run in, container time while the agent works, and what your sites use. - **Your model provider**: the agent's tokens, or your Claude subscription. ## Subscribe - During **Get started**, the **Subscription** step: **Subscribe**, pay on Stripe and come back. The panel confirms the payment (*"Payment received. We are confirming the subscription with Stripe…"*). If it does not show as active yet, wait a moment and **Check again**. - Or from **Account › Subscription › See the subscription › Subscribe to bohr.io Cloud**. - An organization subscribes from its setup, paid by one of its admins. See [Organization settings and billing](https://docs.bohr.io/organizations/settings-and-billing/#subscription-bohrio-cloud). Creating projects on bohr.io Cloud needs an active subscription. Promotion codes are accepted at checkout. ## Manage it **Subscription** (from **Account**) shows the **Status**, the **Plan** and the **Next charge** (or **Valid until** once canceled). **Manage on Stripe** opens Stripe's portal for your card, invoices and cancellation. | Status | Meaning | | --- | --- | | **Active** | Paid and current | | **Trial** | In a trial period | | **Payment overdue** | A payment failed; projects keep working while Stripe retries | | **Canceled**, **Expired**, **Unpaid**, **Payment incomplete**, **Paused** | Not active: you cannot create projects | | **From the previous version, being checked** | A subscription carried over from bohr v1 | ## Customers from bohr v1 Customers who paid for bohr v1 keep **the price of their plan** (*The previous bohr version's price*). Their projects are hosted by bohr until they move them. See [Migrating from bohr v1](https://docs.bohr.io/migrating-from-v1/overview/). --- # Self-hosting overview > Run your own bohr instance on your Cloudflare account, and what it costs. Source: https://docs.bohr.io/self-hosting/overview/ bohr is open source (MIT). A **self-hosted instance** is the same software as bohr.io Cloud, running in your Cloudflare account: one Worker with the API, the onboarding (web and Telegram), the credential vault and the web panel, plus a D1 database. Projects do not run in the instance. Each one runs in its owner's Cloudflare account, which the instance provisions with the owner's API token. Install one instance for yourself, your team or your clients; it has unlimited users and projects and every feature. ## What each person needs | Who | Needs | | --- | --- | | Whoever hosts the instance | A Cloudflare account with Workers, D1 and Workflows (the free plan is enough to start), and a GitHub account to own the instance's GitHub App | | Each user | A Cloudflare account on Workers Paid (the agent sandbox is a Container), a GitHub account, and a model provider. Telegram is optional. | ## Costs | Account | Pays for | | --- | --- | | The instance's | Workers, D1 and Workflows. A small instance fits in the free plan. Workers Paid (US$5/month) is needed only for the optional [sign-in container](https://docs.bohr.io/self-hosting/optional-features/#a-dedicated-sign-in-container). | | Each user's Cloudflare | Workers Paid (US$5/month), the sandbox while the agent works (a `standard-1` container costs cents per hour and sleeps when idle) and what the project uses in production | | Each user's model provider | The agent's tokens. Every request has a budget (US$5 by default, set per project). | ## Steps 1. [Deploy the instance](https://docs.bohr.io/self-hosting/deploy-instance/) with the Deploy to Cloudflare button or with wrangler. 2. [Create its GitHub App](https://docs.bohr.io/self-hosting/github-app/) at `/setup`. 3. Optional: [the platform bot, email sign-in and the sign-in container](https://docs.bohr.io/self-hosting/optional-features/). 4. Sign in: the first person becomes the [administrator](https://docs.bohr.io/self-hosting/administration/). 5. Users go through [Get started](https://docs.bohr.io/getting-started/sign-in/) like on bohr.io Cloud, without the subscription step. ## Trust The instance keeps its users' Cloudflare tokens and its GitHub App's private key, encrypted with a master key you hold. Users must trust whoever hosts the instance. Every use of a stored credential is recorded in the instance's audit log. See [Security model](https://docs.bohr.io/reference/security/). --- # Deploy the instance > Install a bohr instance with the Deploy to Cloudflare button or with wrangler. Source: https://docs.bohr.io/self-hosting/deploy-instance/ ## With the Deploy to Cloudflare button [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/bohr-io/bohr) Cloudflare copies the repository to your GitHub account, creates the D1 database and asks for two secrets: | Secret | Value | | --- | --- | | `ADMIN_TOKEN` | A random string of at least 32 characters (`openssl rand -hex 32`). It protects `/setup` and the admin API. | | `ENCRYPTION_KEY` | 32 random bytes in base64 (`openssl rand -base64 32`). The vault's master key. **Keep a copy**: without it, the stored Cloudflare tokens and GitHub App keys cannot be read. | Leave `APP_URL` empty unless you already know the instance's final address. ## With wrangler ```sh git clone https://github.com/bohr-io/bohr && cd bohr pnpm install npx wrangler login npx wrangler deploy npx wrangler secret put ADMIN_TOKEN # openssl rand -hex 32 npx wrangler secret put ENCRYPTION_KEY # openssl rand -base64 32 ``` You need Node.js 22 and pnpm 10. `wrangler deploy` runs the build in `wrangler.jsonc` (it bundles each project's Worker into the instance and builds the web panel), creates the D1 database on the first deploy and publishes the Worker, named `bohr`. The instance applies its database migrations itself on the first request. To apply them by hand: ```sh npx wrangler d1 migrations apply DB --remote ``` ## The address The instance answers at `https://bohr..workers.dev`, or at a custom domain you add to the Worker. **The address you use to open `/setup` becomes the instance's URL** (unless `APP_URL` is set), and it goes into the GitHub App's callback URLs: use your final domain from the start. Next: [Create the GitHub App](https://docs.bohr.io/self-hosting/github-app/). ## Check it ```sh curl https:///health ``` Opening the instance before `/setup` shows *"This instance is not set up yet"* on the sign-in page, with a link to the setup. --- # The instance's GitHub App > Create the GitHub App your instance uses, and the permissions it asks for. Source: https://docs.bohr.io/self-hosting/github-app/ Every instance has its own GitHub App, created from a manifest at `/setup`. bohr uses it to sign people in with GitHub, read repositories, push the agent's work, open and merge pull requests, and write the deploy workflow. ## Create it 1. Open `https:///setup`. 2. Fill in: - **Admin token**: the `ADMIN_TOKEN` secret. - **App name**: up to 34 characters, unique on GitHub (default `bohr `). - **GitHub organization** (optional): to create the App owned by an organization instead of your account. - **Other people can install it**: keep it checked if other users will bring their own repositories. 3. Click **Create the GitHub App**. GitHub shows the permissions; confirm. GitHub comes back to the instance, which stores the App's credentials in the vault. `/setup` then shows the App and a link to install it. To use another App later, delete the `github_app` setting in the instance's database and open `/setup` again. ## Permissions | Permission | Why | | --- | --- | | Contents: write | The agent pushes to the `homolog` branch; approvals merge to production | | Pull requests: write | Approvals open and merge pull requests; rollbacks open revert pull requests | | Workflows: write | The project Worker writes the deploy workflow (the sandbox's own token never gets this permission) | | Secrets: write | Build variables of projects bohr hosts, and removing Actions secrets that older versions wrote. The Cloudflare token of own-account projects is never stored in GitHub. | | Actions: read | Reports when the production deploy finishes; the Builds page | | Metadata: read | Required by GitHub | | Organization › Members: read | Organizations in bohr mirror the members of the GitHub organization | | Administration: write | Creates repositories from templates (new projects from a template, only with the user's own authorization) | Events: | Event | Why | | --- | --- | | **Organization** | Members added to or removed from a GitHub organization: its bohr organization's members follow | | **Push**, **Pull request** | Subscribed since the first version; bohr acknowledges them and does nothing with them today (deploys run from the repository's workflow) | **Installation** events reach every App without a subscription: bohr records each installation and links a GitHub organization's installation to its bohr organization. The webhook goes to `https:///api/github/webhook`; on a local instance (`http://localhost`) the webhook is off. ## Apps created by older versions - **Organizations** need the **Members** organization permission (read-only) and the **Organization** event. Add them in the App's settings on GitHub (*Permissions & events*); each GitHub organization then accepts the new permission. Until it does, bohr only adds the person who installed the App, and the panel asks for the permission. - **Templates** need the **Administration** repository permission (read and write). Each account that installed the App accepts it once. Until then, GitHub refuses to create the repository and the panel says so. ## Using an existing App Instead of `/setup`, an instance can read its App from secrets: `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` and `GITHUB_WEBHOOK_SECRET`. See [Configuration](https://docs.bohr.io/self-hosting/configuration/). --- # Configuration reference > The secrets, variables and bindings a self-hosted bohr instance reads. Source: https://docs.bohr.io/self-hosting/configuration/ A self-hosted instance is configured by the root `wrangler.jsonc` of the repository and by Worker secrets (`npx wrangler secret put `). For local development, put secrets in `.dev.vars` (git ignores it; `.dev.vars.example` lists them). ## Secrets | Secret | Required | Description | | --- | --- | --- | | `ADMIN_TOKEN` | Yes | Protects `/setup` and the admin API. At least 32 random characters: `openssl rand -hex 32`. | | `ENCRYPTION_KEY` | Yes | The vault's master key: 32 random bytes in base64 (`openssl rand -base64 32`). Accepts several keys while [rotating](https://docs.bohr.io/self-hosting/updates-and-backups/#rotate-the-master-key): `k2:,k1:`, current first. Keep a copy. | | `TELEGRAM_BOT_TOKEN` | No | The [platform bot](https://docs.bohr.io/self-hosting/optional-features/#the-platform-bot) for onboarding on Telegram and linking Telegram accounts. | | `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_WEBHOOK_SECRET` | No | An existing GitHub App, instead of creating one at `/setup`. | ## Variables (`vars`) | Variable | Default | Description | | --- | --- | --- | | `APP_URL` | empty | The instance's public URL. Leave empty to use the address you open `/setup` with. | | `OIDC_AUDIENCE` | `bohr.io` | The audience the instance expects in GitHub OIDC tokens from deploy workflows. Keep `bohr.io`: the deploy action requests that audience. | | `BOHR_TELEMETRY` | unset | `off` (or `0`, `false`) keeps [telemetry](https://docs.bohr.io/self-hosting/telemetry/) off whatever the panel says. | | `EMAIL_FROM` | `bohr >` | The sender of sign-in codes, e.g. `bohr `. Only with the `EMAIL` binding. | | `RUNTIME_IMAGE` | `ghcr.io/bohr-io/agent:` | The [agent image](https://docs.bohr.io/self-hosting/optional-features/#your-own-agent-image) copied into each account that runs projects. Can also be a secret. | ## Bindings | Binding | Type | Description | | --- | --- | --- | | `DB` | D1 | The instance's database (`bohr`). Created by wrangler on the first deploy; migrations in `apps/instance/migrations`. | | `PROVISION` | Workflow (`bohr-provision`) | Sets up each project's runtime in its owner's account. | | `ASSETS` | Static assets | The web panel, built into `apps/web/dist`. | | `EMAIL` | Send email | Optional: [sign-in with a code by email](https://docs.bohr.io/self-hosting/optional-features/#sign-in-with-a-code-by-email). | | `CLAUDE_LOGIN` | Durable Object + container | Optional: the [dedicated sign-in container](https://docs.bohr.io/self-hosting/optional-features/#a-dedicated-sign-in-container). | ## Cron `17 * * * *` (hourly): housekeeping (expired deploy sessions and one-time codes) and, when enabled, the daily telemetry ping. ## Routes the Worker answers first `/api/*`, `/auth/*`, `/setup`, `/setup/*`, `/.well-known/*` and `/health`. Everything else is the web panel (a single-page app). :::note bohr.io Cloud deploys `apps/instance/wrangler.jsonc`, with hosting for migrated v1 projects and billing turned on. Self-hosted instances deploy the root `wrangler.jsonc`, which leaves those off; you do not need any of their settings. ::: --- # Optional features > The Telegram platform bot, sign-in by email, the dedicated sign-in container and your own agent image. Source: https://docs.bohr.io/self-hosting/optional-features/ ## The platform bot Users can go through onboarding on the web. A **platform bot** also lets them do it on Telegram (`/new`, `/status`, `/retry`), link their Telegram account to their bohr account, and create each project's bot with one tap. 1. Create a bot with [@BotFather](https://t.me/BotFather). In its settings, allow it to **manage other bots** (Bot API managed bots): that lets it create each project's bot. Without it, users paste a BotFather token instead. 2. Store its token: ```sh npx wrangler secret put TELEGRAM_BOT_TOKEN ``` 3. Point the bot at the instance: ```sh curl -X POST https:///api/admin/platform-bot \ -H "Authorization: Bearer " ``` Without a platform bot, **Link Telegram** says *"This instance has no Telegram bot set up."*; project bots still work with BotFather tokens. ## Sign-in with a code by email Users can sign in with GitHub, or with a one-time code sent through [Cloudflare Email Service](https://developers.cloudflare.com/email-service/). 1. Enable Email Sending for a domain of yours: ```sh npx wrangler email sending enable mail.example.com ``` 2. In `wrangler.jsonc`, uncomment the `send_email` binding and set `EMAIL_FROM`: ```jsonc "send_email": [{ "name": "EMAIL", "allowed_sender_addresses": ["login@mail.example.com"] }], "vars": { "EMAIL_FROM": "bohr " } ``` 3. Deploy again. Without the binding, the sign-in page only offers GitHub. ## A dedicated sign-in container Connecting a [Claude subscription](https://docs.bohr.io/agent/providers/#claude-subscription-promax) runs the official CLI's `claude setup-token`. Without anything else, it runs in the container of one of the owner's projects: the owner needs a project whose agent is ready, and a sleeping container has to start first. A small dedicated container attached to the instance removes both: the connection works before any project exists and starts in seconds. It gets no secrets; the token it returns goes to the instance, never to the browser. Each user or organization signing in gets its own container, which stops 10 minutes after the last use. It needs Workers Paid on the instance's account and the image in that account's registry: 1. Create an API token with **Containers: Edit** on the instance's account and copy the image (the version in `runtime/claude-login/package.json` by default; pass another image as an argument): ```sh CLOUDFLARE_API_TOKEN= CLOUDFLARE_ACCOUNT_ID= node apps/instance/scripts/copy-login-image.mjs ``` 2. In `wrangler.jsonc`, uncomment `durable_objects`, `migrations` and `containers` under *"Optional: a small dedicated container"*, with your account ID in the image (the script prints the line), and deploy again. Copy the new image and update its tag when you update bohr. ## Your own agent image Each project's sandbox runs the image built from `runtime/agent`. On the first project of an account, the instance copies the published image (`ghcr.io/bohr-io/agent:`) into that account's Cloudflare registry. To use your own build: ```sh pnpm --filter @bohr/agent build docker build --platform linux/amd64 -f runtime/agent/Dockerfile -t ghcr.io//bohr-agent: . docker push ghcr.io//bohr-agent: npx wrangler secret put RUNTIME_IMAGE # ghcr.io//bohr-agent: ``` The image must be public (GitHub Container Registry or Google Artifact Registry recommended; Docker Hub limits anonymous pulls). Cloudflare Containers run `linux/amd64` images. --- # Administration > Instance administrators, the Admin area of the panel, and the admin API. Source: https://docs.bohr.io/self-hosting/administration/ ## Administrators The **first person who signs in** to a new instance becomes its administrator. Administrators: - see the **Admin** area in the panel; - have owner-level access to every project and organization (the project shows *"You are viewing this project as an administrator"*), and their changes are recorded as an administrator's in the audit log; - update the project runtimes and choose whether the instance sends [telemetry](https://docs.bohr.io/self-hosting/telemetry/). Tokens for the CLI never carry the administrator role. ## The Admin area | Page | | | --- | --- | | **Overview** | Users, projects and active subscriptions, and **Runtimes**: **Update runtimes** brings every project's Worker to this instance's version | | **Users** | Search by name, email or username. A user's page lists their projects and organizations, **Make admin** / **Remove admin**, and deleting their account | | **Projects** | Search by name, repository or owner; status, hosting and runtime of each | | **Organizations** | Search by name or GitHub login; members, projects and subscription | | **Telemetry** | The opt-in switch and exactly what would be sent | The instance needs at least one administrator: *"The instance needs at least one admin."* ## The admin API Calls with `Authorization: Bearer `: ```sh # Make someone an administrator (or "user" to remove it); identify them by email, githubLogin or userId curl -X POST https:///api/admin/users/role \ -H "Authorization: Bearer " -H "Content-Type: application/json" \ -d '{"email":"you@example.com","role":"admin"}' # Move every project runtime to this instance's version curl -X POST https:///api/admin/runtimes/update -H "Authorization: Bearer " # Point the platform bot at the instance (after setting TELEGRAM_BOT_TOKEN) curl -X POST https:///api/admin/platform-bot -H "Authorization: Bearer " # Re-seal every stored credential under the current master key (after a key rotation) curl -X POST https:///api/admin/vault/rewrap -H "Authorization: Bearer " ``` Keep `ADMIN_TOKEN` secret: it opens `/setup` and these calls. --- # Updates and backups > Update your instance and the project runtimes, back up its database, and rotate the master key. Source: https://docs.bohr.io/self-hosting/updates-and-backups/ ## Update the instance Pull the upstream changes into your copy of the repository and deploy again: ```sh git pull upstream main pnpm install npx wrangler deploy ``` If you used the Deploy to Cloudflare button, push the changes to the branch Workers Builds deploys. New database migrations run on the first request after the deploy. ## Update the project runtimes Each project's Worker (the agent, the bot and the preview) runs the version it was created with. After updating the instance, bring them all to the new version: - **Admin › Overview › Update runtimes**, or - the admin API: ```sh curl -X POST https:///api/admin/runtimes/update -H "Authorization: Bearer " ``` While a runtime updates, the previous version keeps serving; a failed update leaves it running as it was. If you use the [dedicated sign-in container](https://docs.bohr.io/self-hosting/optional-features/#a-dedicated-sign-in-container), copy its new image too. ## Back up What to keep safe: | What | Where | How | | --- | --- | --- | | `ENCRYPTION_KEY` | A Worker secret | Keep a copy outside Cloudflare (a password manager). Without it, stored Cloudflare tokens, GitHub App keys and agent keys cannot be read. | | `ADMIN_TOKEN` | A Worker secret | Keep a copy; you can also replace it with `wrangler secret put`. | | The instance's database | D1 `bohr` | `npx wrangler d1 export bohr --remote --output bohr-backup.sql`. D1's Time Travel can also restore it to a recent point in time. | The instance's database holds users, sessions, projects, organizations, connections (encrypted) and settings. Each project's members, chat and releases live in the project's Durable Object, in its owner's Cloudflare account; the code lives in GitHub. ## Rotate the master key `ENCRYPTION_KEY` accepts several keys, current first: `k2:,k1:`. New values are sealed with `k2`; old ones still open with `k1`. To move everything to `k2`, then drop `k1`: ```sh npx wrangler secret put ENCRYPTION_KEY # k2:,k1: curl -X POST https:///api/admin/vault/rewrap -H "Authorization: Bearer " npx wrangler secret put ENCRYPTION_KEY # k2: ``` --- # Telemetry > The anonymous, opt-in telemetry of self-hosted instances, what it sends and how to keep it off. Source: https://docs.bohr.io/self-hosting/telemetry/ Self-hosted instances can send anonymous usage telemetry. It is **off by default**: an instance administrator turns it on in **Admin › Telemetry**, which shows exactly what is sent. ## What is sent ```json { "instanceId": "3f1c2a9e-8b7d-4c1e-9a2f-6d5e4b3a2c1d", "version": "2.0.0", "projects": 12 } ``` - `instanceId`: a random ID created for this purpose; it identifies nothing but the instance. - `version`: the instance's version. - `projects`: how many projects it has. Nothing else: no names, emails, URLs, repositories or IP-derived data are stored. ## When and where Once a day, from the instance's hourly cron, as a `POST` to `https://app.bohr.io/api/telemetry`. The first ping goes out right away when you turn it on, so **Last sent** shows it worked. ## Why To know how many instances and versions are out there, so bohr knows what to keep compatible. ## Turning it off - Switch **Send anonymous telemetry** off in **Admin › Telemetry**, or - set the variable `BOHR_TELEMETRY=off` in `wrangler.jsonc` and deploy. It keeps telemetry off whatever the panel says; the panel then shows *"Telemetry is turned off in this instance's configuration (BOHR_TELEMETRY=off)"*. bohr.io Cloud never sends telemetry; it only receives these pings. --- # bohr v1 projects in bohr v2 > What happened to bohr v1 projects, and what you can do now. Source: https://docs.bohr.io/migrating-from-v1/overview/ bohr v2 replaces bohr v1. The projects of paying bohr v1 customers were moved to bohr v2 for them; you did not need to do anything. ## What happened to your project - **It runs on bohr v2**, hosted by bohr in its own Cloudflare account, as it was in v1. In the panel, its hosting shows **Hosted by bohr**. - **Same addresses, no DNS changes.** Your custom domains and your `bohr.io` addresses (`-.bohr.io`) keep working. - **Your repository keeps its workflow.** The bohr v1 workflow (`.github/workflows/bohr.yml`) keeps running on every push, and now deploys to bohr v2. bohr wrote nothing into your repository: no new workflow, no files, no Actions secrets. - **Same price.** Your plan keeps the price you paid in v1. - **Your settings came along**: environment variables (as [build variables](https://docs.bohr.io/projects/variables/#build-variables) and [site variables](https://docs.bohr.io/projects/variables/#site-variables)), password protection and domains. Sign in at [app.bohr.io](https://app.bohr.io) with the same GitHub account to see your projects. ## What you can do now | | | | --- | --- | | **Keep it as it is** | Push as before; every branch still gets its own preview address. See [What changes](https://docs.bohr.io/migrating-from-v1/what-changes/). | | **Turn the agent on** | Change the site by conversation, with previews and approvals. On the project, click **Turn the agent on** (needs an active subscription, and a [model provider](https://docs.bohr.io/agent/providers/)). | | **Move it to your own Cloudflare account** | Where bohr v2 projects run. See [Move to your Cloudflare account](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/). | | **Deploy from your terminal or an AI agent** | `bohr deploy` publishes projects bohr hosts. See [the CLI](https://docs.bohr.io/cli/deploy/). | Questions: [Discord](https://discord.com/invite/p3hhfGg2Uy) or [contact@bohr.io](mailto:contact@bohr.io). See also the [FAQ](https://docs.bohr.io/migrating-from-v1/faq/). --- # What changes for a v1 project > How a migrated project builds, deploys and is served while bohr hosts it. Source: https://docs.bohr.io/migrating-from-v1/what-changes/ ## Deploys keep working Your repository's v1 workflow runs `bohr-io/action` on every push, as before. That action now asks bohr v2 how to build the project (its **build plan**: the Node.js version, the install and build commands and the generated files bohr v2 needs), builds it, and deploys with [`bohr-io/deploy-action`](https://docs.bohr.io/github-action/reference/#build-plan-mode). The build plan lives in bohr, not in your repository. - **Every pushed branch** is deployed to its own address: `--.bohr.io`. The production branch gets `-.bohr.io` and your custom domains. - **Build variables** are handed to the build when it runs; manage them in **Settings › Environment variables**. Pull requests never get them. - **Static files are published as v1 published them**: files v1 left out (such as `node_modules/`, `.github/`, `package.json`, lockfiles and `.env` files) are still left out, and files of 24 MiB or more are skipped with a warning instead of failing the deploy. ## What the hosting supports Projects bohr hosts are Workers with static assets in bohr's account: - Static assets, `vars` and secrets ([site variables](https://docs.bohr.io/projects/variables/)). Other bindings (D1, KV, R2, Durable Objects…) and cron triggers are not available while bohr hosts the project; [move it](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/) to use them. - [Password protection](https://docs.bohr.io/projects/password-protection/) is applied by bohr's edge. - Domains stay as they were in v1. To change them, move the project to your account. ## The panel - **Settings › Hosting** shows **Hosted by bohr** and the move. - **Settings › Domains** lists your domains as **Live** or **Pending** (*Pending* while bohr v1 still serves an address). - **Builds** shows the runs of your workflow. - Without the agent, the project shows **Deploys only**. **Turn the agent on** adds the agent, the preview and approvals; the agent then runs in bohr's account too, until you move the project. ## The CLI bohr v2's CLI is `bohr@2`. Your v1 workflow does not need it. Locally: - `bohr deploy` publishes a preview of the current branch; `bohr deploy --prod` publishes production (owners only). See [bohr deploy](https://docs.bohr.io/cli/deploy/). - While bohr v1 still serves your production address, production deploys come from your workflow (`production_on_v1`); previews work. - v1 commands (`bohr dev`) and v1 options (`--no-install`, `--show-install`, `--show-build`) are gone. - In a repository that still runs only on bohr v1, the CLI hands the command to bohr v1's CLI, so it deploys as it always did. --- # Move to your Cloudflare account > Move a project bohr hosts to your own Cloudflare account, with its secrets and domains. Source: https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/ bohr v2 projects run in their owner's Cloudflare account. A project migrated from v1 can move there whenever you want: you get every Cloudflare feature (D1, KV, R2, Durable Objects, cron triggers…) and full control of your domains. ## Before you start - A Cloudflare account on **Workers Paid**, connected to bohr (or connect one during the move). See [Connect Cloudflare](https://docs.bohr.io/getting-started/connect-cloudflare/). - For each custom domain to move without downtime, its **zone in that Cloudflare account** (nameservers pointing to Cloudflare). - Only the project's owner (or an admin of its organization) can move it. :::caution[Not automated for every project yet] The automatic move works for repositories whose deploy workflow the migration generated. Repositories that still run bohr v1's workflow (`bohr.yml` with `bohr-io/action`) cannot be moved automatically yet: the panel says *"The move could not be prepared automatically."* See [below](#when-the-move-cannot-be-prepared-automatically). ::: ## How it works 1. bohr opens a pull request in your repository that switches the deploy to your Cloudflare account. 2. After the merge, GitHub Actions publishes the site in your account, and bohr copies the secrets and moves your domains. 3. The bohr.io addresses start redirecting to the site's new address. The deploy gets its Cloudflare token from bohr at run time, never from the repository. ## Step by step 1. Open the project's **Settings › Hosting**. 2. Under **Destination Cloudflare account**, pick an account, or **Connect another account**. 3. Click **Prepare the move** and confirm. bohr opens a pull request (branch `bohr/move-to-cloudflare`) that changes the deploy workflow to `wrangler deploy` to your account, on the production branch, and writes your plain (non-secret) production variables into `wrangler.jsonc`. The site only changes after the merge. 4. Review and merge the pull request on GitHub (**Open #N on GitHub**). 5. Wait for the GitHub Actions deploy to finish. 6. Come back and click **Finish the move**. **Finish the move** checks the merge and the new Worker, then copies the secrets to it, attaches your custom domains whose zones are in your account, and points the bohr.io addresses at the new home. If it says *"It cannot be finished yet"*, check that the pull request is merged and the deploy finished, and try again. ## The addresses afterwards | Status | Meaning | | --- | --- | | **In your account** | The domain now points at the Worker in your account | | **Redirects** | The bohr.io addresses redirect (301) to the site's new address | | **Pending** | The domain's zone is not in your account yet. Add it, point its nameservers to Cloudflare, and click **Finish the move** again. Until then, it keeps showing the last version bohr published. | | **Turned off** | Branch preview addresses are no longer deployed: in your account, previews come from the agent | When nothing is pending: *"Done! The project now runs in your Cloudflare account."* From then on it is a regular bohr v2 project: [domains](https://docs.bohr.io/projects/domains/), [variables](https://docs.bohr.io/projects/variables/) and the [deploy workflow](https://docs.bohr.io/deploys/deploy-workflow/) work as for any project in your account. If the agent was on, it keeps working: its sandbox stays where it was created, and from now on it deploys to your account. ## When the move cannot be prepared automatically Some repositories cannot be moved automatically, for example when the deploy workflow is not the one the migration generated, or the repository still runs bohr v1's workflow with a build plan. The panel then says *"The move could not be prepared automatically."* Talk to us on [Discord](https://discord.com/invite/p3hhfGg2Uy) or at [contact@bohr.io](mailto:contact@bohr.io): we help you change the deploy step by hand. --- # Migration FAQ > Common questions from bohr v1 customers. Source: https://docs.bohr.io/migrating-from-v1/faq/ ### Do I need to do anything? No. Your project was moved to bohr v2 for you and keeps deploying from your repository's workflow. Moving it to your own Cloudflare account and turning the agent on are optional. ### Did my price change? No. Customers who paid for bohr v1 keep the price of their plan. The subscription page shows *The previous bohr version's price*. ### Did my domains or DNS change? No. Your custom domains and `bohr.io` addresses keep working, with no DNS changes. If you [move the project](https://docs.bohr.io/migrating-from-v1/move-to-your-cloudflare/), each domain moves when its zone is in your Cloudflare account. ### Did bohr change my repository? No. The migration wrote nothing into your repository: no workflow, no files, no secrets. The move to your own account, if you start it, opens a pull request you review and merge. ### My workflow still says `bohr-io/action@main`. Is that right? Yes. That action now builds and deploys your project on bohr v2. Leave it as it is while bohr hosts the project. ### Where are my environment variables? In **Settings › Environment variables**: build variables for the GitHub Actions build, and site variables for the running site. Secret values are hidden. ### Do branch previews still work? Yes, while bohr hosts the project: every pushed branch gets `--.bohr.io`. After a move to your account, previews come from the agent (the `homolog` branch) instead. ### Can I use KV, D1, R2 or cron triggers? Not while bohr hosts the project: hosted projects support static assets, `vars` and secrets. Move the project to your Cloudflare account to use any Cloudflare binding. ### `npx bohr deploy` stopped working in my v1 workflow or terminal. bohr v2's CLI does not deploy bohr v1 projects or accept v1 options. Your v1 workflow does not need it: it deploys through `bohr-io/action`. Locally, use `bohr deploy` from bohr v2 ([docs](https://docs.bohr.io/cli/deploy/)). ### A file larger than 24 MiB disappeared from my site. bohr v1 never published files of 24 MiB or more, and migrated projects keep that rule. Cloudflare's own limit is 25 MiB per file. Serve large files from storage such as R2 instead. ### Who do I talk to? [Discord](https://discord.com/invite/p3hhfGg2Uy) or [contact@bohr.io](mailto:contact@bohr.io). --- # Glossary > The words bohr uses, in English and in the Portuguese panel. Source: https://docs.bohr.io/reference/glossary/ | Term | Portuguese panel | Meaning | | --- | --- | --- | | **Instance** | Instância | The bohr control plane: the panel, the API and the credential vault. bohr.io Cloud is one; you can [host your own](https://docs.bohr.io/self-hosting/overview/). | | **Project** | Projeto | One GitHub repository, its production site and its agent. | | **Project Worker** | Worker do projeto | The Worker bohr creates in the owner's Cloudflare account for each project: the request queue, members, chat, the Telegram bot and the agent's sandbox. Not your site. | | **Sandbox** | Sandbox | The Cloudflare Container where the agent works on a copy of the repository and runs the preview. It sleeps when idle. | | **Agent** | Agente | The AI that changes your code from a conversation. | | **Request** | Pedido | One thing a member asked the agent. Requests run one at a time. | | **Preview** | Homologação | The site running from the `homolog` branch, with changes waiting for a decision. | | **`homolog`** | — | The branch the agent commits to. | | **Version** | Versão | Each set of changes the agent pushed to the preview (version 1, 2…), and each release in **Versions**. | | **Approve and publish** | Aprovar e publicar | Merge the preview into the production branch, which deploys production. | | **Discard** | Descartar | Reset the preview to production, dropping its changes. | | **Release** | Publicação | A merged approval or rollback, deployed by GitHub Actions. | | **Rollback** | Reverter | A release that reverts the last one. | | **Production branch** | Branch de produção | The branch production deploys from, usually `main`. | | **Owner** | Dono | Manages a project's settings, keys and access. In an organization, its admins. | | **Member** | Membro | Asks for changes and approves releases. | | **Organization** | Organização | A GitHub organization with bohr's app installed; its members mirror GitHub's. | | **Model provider** | Provedor do modelo | Where the agent's model comes from: Anthropic, a compatible endpoint, or a Claude subscription. | | **Site variables** | Variáveis do site | Values the production Worker reads at runtime. | | **Build variables** | Variáveis do build | Values the GitHub Actions build reads, kept encrypted in bohr. | | **Hosted by bohr** | Hospedado pela bohr | A project migrated from bohr v1, still in bohr's Cloudflare account (`mode: managed`). | | **Own account** | Sua conta da Cloudflare | A project in its owner's Cloudflare account (`mode: byo`). | | **Build plan** | — | How bohr v2 builds a migrated project that keeps its v1 workflow. | | **Token** | Token | A `bohr_…` credential for the CLI, scripts and AI agents. | | **Deploy workflow** | — | `.github/workflows/bohr-deploy.yml`, written by bohr from `.bohr/deploy.json`. | | **Platform bot** | — | The instance's own Telegram bot, for onboarding and linking accounts. Each project has its own **project bot**. | --- # Troubleshooting > Common errors, with the exact messages bohr shows, and how to fix them. Source: https://docs.bohr.io/reference/troubleshooting/ ## Sign-in | Message | Fix | | --- | --- | | *This instance is not set up yet* | Self-hosted: the administrator must finish `/setup` ([GitHub App](https://docs.bohr.io/self-hosting/github-app/)). | | *Too many attempts. Wait a few minutes and try again.* | Too many email codes or wrong codes. Wait about 15 minutes. | | *The code expired or was entered wrong too many times. Ask for a new code.* | Codes last 10 minutes and allow 5 attempts. | | *This email already belongs to another bohr account.* | Sign in with that account instead, or use another address. | ## Connecting Cloudflare | Message | Fix | | --- | --- | | *That does not look like a Cloudflare API token.* | Copy the whole token, without spaces. | | *Cloudflare did not accept this token.* / *This token is …* | The token is invalid, expired or disabled. Create another one. | | *The token lacks some permissions* (Containers: Edit…) | Edit the token and add what is listed. **Containers: Edit** also needs the account to be on **Workers Paid**. See [Connect Cloudflare](https://docs.bohr.io/getting-started/connect-cloudflare/). | | *This token cannot see any Cloudflare account.* | Give the token access to your account (Account Resources). | | *Paste the Cloudflare token again.* | You took more than 15 minutes to pick an account. Paste the token again. | | *The Cloudflare token cannot create D1 databases.* | Template projects need **D1: Edit**. Edit the token and try again. | ## Project setup | Message | Fix | | --- | --- | | *This Cloudflare account has no workers.dev subdomain yet. Open Workers & Pages in the Cloudflare dashboard once to create it, then try again.* | Do that, then use the retry on the project page. | | *… is already a bohr project.* | A repository can only be one project. Look for it in **Projects** (or in an organization). | | *The GitHub App cannot reach that repository.* | [Give the app access](https://docs.bohr.io/getting-started/install-github-app/#give-access-to-more-repositories) to the repository. | | *Creating projects on bohr.io Cloud needs an active subscription.* | [Subscribe](https://docs.bohr.io/billing/subscription/), or ask an organization admin to. | | *The Cloudflare account already has a Worker named … Choose another project name.* | Template projects: pick another name. | | *GitHub is taking too long to copy the template.* | The repository was created; wait a minute and create the project from it as a repository. | | *Setup failed* with another reason | Check the token's permissions and that the account is on Workers Paid, then retry. | ## The agent | Message | Fix | | --- | --- | | *The agent is off because it has no provider yet.* | The owner (or organization admin) sets a [model provider](https://docs.bohr.io/agent/providers/). | | *That does not look like an Anthropic API key (sk-ant-…).* | Anthropic keys start with `sk-ant-`. A `sk-ant-oat01-` token is a Claude subscription token: choose **Claude subscription**. | | *The provider did not accept this key.* / *Could not reach …* | Check the key, and the **Base URL** of a compatible provider (without `/v1/messages`). | | *the request's budget (US$…) ran out before the work was done* | Raise **Budget per request** in **Settings › Agent**, or split the request. | | *the agent's workspace restarted in the middle of the request* | It happens during bohr updates. Send the request again; an interrupted repository preparation restarts by itself once. | | *You already have 5 requests in the queue.* | Wait, or cancel one with `/cancel`. | | *That message is too long.* | Requests are limited to 20,000 characters. Split it. | ## The preview | Message | Fix | | --- | --- | | *The preview is not set up yet: the agent prepares it when it imports the repository.* | Wait for the repository preparation, or click **Prepare the repository** in the chat. | | *The preview could not start. Ask the agent to check the file .bohr/preview.json.* | Open **Preview status and log** to see the error, and ask the agent to fix it. See [.bohr/preview.json](https://docs.bohr.io/deploys/preview-json/). | | *The preview is starting. This page reloads by itself.* | The sandbox was asleep and is installing dependencies. It can take a few minutes. | | *The preview is running, but its server did not answer.* | The dev server must listen on `0.0.0.0` at the configured port and accept any `Host` header. | ## Deploys | Message | Fix | | --- | --- | | *The production deploy failed. Details: …* | Open the run in [Builds](https://docs.bohr.io/projects/builds/) and ask the agent to fix the error. Production keeps the previous version. | | *The file .bohr/deploy.json has a problem (…), so the production deploy was not updated.* | Ask the agent to fix it, or fix it by hand ([reference](https://docs.bohr.io/deploys/deploy-json/)). | | `Deploy credentials are only given to .github/workflows/bohr-deploy.yml on .` | Only that workflow, on a push to the production branch, gets credentials. | | `Could not get a GitHub OIDC token` | The workflow needs `permissions: id-token: write`. | | *Protection does not apply to this site* | The project uses a custom `deploy` command; see [Password protection](https://docs.bohr.io/projects/password-protection/). | ## CLI | Message | Fix | | --- | --- | | `Not signed in to … Run "bohr login", or set BOHR_TOKEN…` | Sign in, or set a token ([Authentication](https://docs.bohr.io/cli/authentication/)). | | `… The token expired or was revoked…` | Create a new token, or run `bohr login` again. | | `… runs in its owner's Cloudflare account, and deploys from the CLI only reach projects bohr hosts for now.` | An outdated CLI (4.0.0 or earlier): run `npx bohr@latest` ([details](https://docs.bohr.io/cli/deploy/#which-projects)). | | `… runs in its owner's Cloudflare account: only its owners (admins of its organization) can deploy it from the CLI…` | Only owners deploy those projects from the CLI. Push a branch instead, or ask an owner ([details](https://docs.bohr.io/cli/deploy/#who-can-deploy)). | | `wrangler printed no preview address: turn on preview URLs for the Worker…` | Set `"preview_urls": true` in the Worker's wrangler config ([details](https://docs.bohr.io/cli/deploy/#previews)). | | `This folder's wrangler config deploys the Worker "…", but …'s site is "…"` | Run it in the folder of the project's Worker, or fix `name` in the wrangler config. | | `This repository is not a bohr v2 project: …` | Run it in the project's folder, link it with `bohr link`, or add a wrangler config. bohr v1 repositories are never touched. | | `Which project? … has no GitHub remote (origin).` | Pass `--project ` or run `bohr link`. | | `Which branch? HEAD is detached: pass --branch .` | Pass `--branch`. | | `Asset too large: …` | Cloudflare accepts files up to 25 MiB: remove it or list it in `.assetsignore`. | | `Found wrangler.toml, but this action reads wrangler.jsonc.` | Convert the config to `wrangler.jsonc`. | All CLI codes: [Exit codes and errors](https://docs.bohr.io/cli/exit-codes/). ## Domains and variables | Message | Fix | | --- | --- | | *The site is not published yet* / `The site's Worker (…) is not deployed yet.` | Domains and site variables need the site's Worker: publish once. | | `No zone in your Cloudflare account covers … Add the domain to Cloudflare first.` | Add the domain's zone to the account the project runs in. | | *… is a name bohr or GitHub already use.* | Names starting with `GITHUB_`, `CLOUDFLARE_`, `BOHR_`, and `CI`, are reserved. | | `… is already set in the site's wrangler config: change it there.` | Remove it from `vars` in `wrangler.jsonc`, or change it there. | Still stuck? [Discord](https://discord.com/invite/p3hhfGg2Uy) or [contact@bohr.io](mailto:contact@bohr.io). --- # Limits > Every size, time and count limit in bohr, in one place. Source: https://docs.bohr.io/reference/limits/ ## The agent | Limit | Value | | --- | --- | | Budget per request | US$5 by default; owner sets it (0.10 to 1000, in the panel or on Telegram) | | Maximum turns per request | 200 by default; 1 to 1000 | | Running time per request | 2 hours | | Request length | 20,000 characters | | Waiting requests | 5 per member; one runs at a time per project | | Questions from the agent | Up to 8 options | | Preview start | The dev server must accept connections within 3 minutes; `install` within 15 minutes | | Reserved port | 4840 (the agent's control server) | ## Deploys | Limit | Value | | --- | --- | | Static file size | 25 MiB per file (Cloudflare). Migrated v1 projects skip files of 24 MiB or more. | | `_redirects`, `_headers` | 256 KiB each (projects bohr hosts) | | Deploy workflow | 30 minutes per run; one run at a time per repository | | `.bohr/deploy.json` commands | One line, up to 500 characters, no `${{ }}` | | `.bohr/deploy.json` `secrets` | Up to 50 names | | `.bohr/preview.json` commands | Up to 1000 characters | | Deploy sessions (CLI and action) | Expire after 2 hours | | Preview branches a token creates | 20 per project bohr hosts, counting its existing previews; a branch that has a preview keeps deploying | | Projects per repository | One | ## Accounts and access | Limit | Value | | --- | --- | | Session in the panel | 30 days | | Email sign-in code | 6 digits, valid 10 minutes, 5 attempts | | `bohr login` code | Valid 10 minutes | | Project invite code | Single use, valid 24 hours | | Tokens per user | 50 | | Token expiry | 30, 90 or 365 days, or never | | Telegram budget command | 0.10 to 1000 (US$) | ## Projects bohr hosts Static assets, `vars` (strings) and secrets only; no other bindings and no cron triggers. See [Wrangler config](https://docs.bohr.io/deploys/wrangler-config/#projects-hosted-by-bohr). --- # Security model > What bohr can access, how credentials are stored, and why the agent cannot publish or leak your keys. Source: https://docs.bohr.io/reference/security/ ## What bohr can access | Access | Given by | Used for | | --- | --- | --- | | The repositories you select | bohr's GitHub App ([permissions](https://docs.bohr.io/getting-started/install-github-app/#what-the-app-can-do)) | The agent's branch, pull requests, merges, the deploy workflow, build logs | | Your Cloudflare account | The API token you create ([permissions](https://docs.bohr.io/getting-started/connect-cloudflare/#create-the-token)) | Creating and updating each project's Workers, sandbox and preview; your production deploy; domains, variables and protection | | Your model provider | Your API key or Claude subscription token | The agent's requests | ## How credentials are stored | Credential | Stored | Notes | | --- | --- | --- | | Cloudflare API token | In the instance's vault, envelope-encrypted (AES-256-GCM) | Every use is recorded in the instance's audit log. Handed to your production deploy at run time (and to an owner's `bohr deploy`), never stored in GitHub. | | GitHub App private key | In the instance's vault | Used for short-lived installation tokens scoped to one repository | | Model key or Claude token | Encrypted in the instance, and as a secret of each project Worker | The sandbox only ever holds a placeholder | | Build variables | Encrypted in the instance | Bound to their project, branch and name | | Site variables | Secrets of your production Worker | bohr keeps plain values only to show them | | Site passwords | PBKDF2-SHA256 hashes only | — | | CLI and API tokens (`bohr_…`) | Only their SHA-256 hash | The token itself lives on your computer or in `BOHR_TOKEN` | | Invite codes | Hashed | Single use, 24 hours | ## The agent's sandbox The sandbox is a Cloudflare Container in the project owner's account. It **never holds a credential**: - The agent sees placeholders. A gateway in the project Worker adds the real model key, only to the provider's message endpoints and only while a request runs, not while the preview runs between requests. - Git traffic gets a short-lived token for that one repository, with **contents: write** and nothing else. Pushes are accepted only while a request runs, and only to `homolog`. - Without the workflows permission, GitHub refuses any push that changes `.github/workflows`, so code in the sandbox cannot add a workflow that would run with the repository's secrets. - Requests carrying credentials do not follow redirects, so a redirect cannot carry a key elsewhere. - A malicious dependency installed in the sandbox can use the network, but cannot read the keys. The one exception: connecting a Claude subscription runs `claude setup-token` in a container, where the new token exists until the sign-in ends. Instances with the dedicated sign-in container run it there, where no project code runs. ## Approvals - Production changes only through an approved, merged pull request. The merge names the reviewed commit: changes pushed after a review cannot be published by that approval. - The diff link in each decision shows exactly the commit that approving publishes. - The preview is public but unlisted (an unguessable `workers.dev` name, `noindex`), and never reaches the sandbox's control port. ## Deploy credentials Projects in your own account deploy from GitHub Actions with `wrangler deploy`, and **no Cloudflare token lives in the repository**. The workflow's first bohr step requests a GitHub OIDC token (audience `bohr.io`) and asks the instance for credentials. The instance verifies the token (issuer, audience, expiry, signature), finds the project by its repository id, and answers only when: - the run is `.github/workflows/bohr-deploy.yml` on the production branch, triggered by `push` or `workflow_dispatch` (no pull request, other branch or other workflow); - the project runs in its owner's Cloudflare account, with a connection of its owner (the user's, or the organization's). Every answer is audited (repository, ref, run id; never the token). Every refusal is an HTTP 403 with the reason. Code that runs in the production workflow comes only from approved merges or people with write access to the production branch. ## Tokens A `bohr_…` token acts as its user, only on the projects it lists, and never with the instance administrator role. Deploy sessions opened with a token can only be finished with the same token. For [`bohr deploy`](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account) of a project in its owner's Cloudflare account, a token can also obtain that account's Cloudflare API token and account id, which wrangler needs. The instance answers only when: - the token reaches the project and its user is an owner of the project (for an organization's project: an admin of the organization); - the project runs in its owner's Cloudflare account, with a connection of its owner. Never for projects bohr hosts, and never the token of bohr's own account. Every issuance is recorded in the audit log (user, token, project), answered with `Cache-Control: no-store`, and limited to 30 per user and project in 10 minutes (HTTP 429 past that). The CLI puts the Cloudflare token only in wrangler's environment: never on a command line, in a log or on disk. ## What bohr leaves in your Cloudflare account Every API call sends `User-Agent: bohr/ (+https://bohr.io)`; bohr's Workers carry the tags `bohr` and `bohr-project:`; production deploys, and the versions `bohr deploy` uploads, carry the message `bohr` and the tag `bohr-` (`bohr-local` for a working tree with uncommitted changes). ## Reporting a vulnerability Email [contact@bohr.io](mailto:contact@bohr.io). Please do not open public issues for security problems. We answer within 3 business days. --- # Changelog > What changed in bohr, version by version, starting with bohr v2. Source: https://docs.bohr.io/changelog/ ## CLI 4.1.0 - `bohr deploy` deploys projects in your own Cloudflare account, for their owners (admins of the organization): previews as Worker versions with a preview alias per branch, production with the D1 migrations first, both with wrangler and the build variables kept in bohr. Members get exit code 5 (`own_account_not_allowed`). See [Projects in your own Cloudflare account](https://docs.bohr.io/cli/deploy/#projects-in-your-own-cloudflare-account). - The `--json` result of `bohr deploy` has `mode` (`managed` or `byo`); deploys in your own account add `versionId`, `previewUrl`, `workerName` and `commitSha`. - `own_account` (exit code 5) now only comes from CLI 4.0.0 and earlier: update with `npx bohr@latest`. ## 2.0.0 bohr v2: a rewrite of bohr as an open source, self-hosted deploy platform operated by an AI agent. ### The agent - An AI agent changes your site by conversation, in the web panel or on a Telegram bot per project. - Every change goes to a preview (`homolog` branch) first; **Approve and publish** merges it to production, **Discard** drops it, and rollback reverts the last release. - Model providers: an Anthropic API key, any endpoint compatible with the Anthropic Messages API, or a Claude Pro/Max subscription. - Budget and maximum turns per request, model and language per project. ### Projects - Projects run in their owner's Cloudflare account, deployed by a GitHub Actions workflow bohr writes from `.bohr/deploy.json`, with credentials handed out at run time through GitHub OIDC. - New projects from six templates (Waitlist, Booking, Link in bio, Forms and surveys, Digital menu, Short links), with a D1 database. - Site and build variables, Basic Auth password protection, custom domains, builds and versions in the panel. - Members with single-use invite codes; organizations that mirror GitHub organizations, with their own subscription, Cloudflare account and agent. ### Deploy tools - The `bohr` CLI 4.0.0 (`npx bohr`; versions 2.x and 3.x on npm are an unrelated 2016 package that used the name): `login`, `logout`, `whoami`, `projects`, `link`, `open` and `deploy`, with `--json` output and stable exit codes for scripts and AI agents. - Tokens in **Account › Tokens**, scoped to projects and with an expiry. - `bohr-io/deploy-action@v1` runs the CLI in GitHub Actions. ### Running bohr - Self-hosting with the Deploy to Cloudflare button, opt-in anonymous telemetry. - bohr.io Cloud at app.bohr.io for US$20/month per account or organization. - Projects from bohr v1 keep deploying from their workflow, hosted by bohr until their owners move them to their own Cloudflare account.