WEBINARSandboxes for AI agents, Oct 7th.Sandboxes: give your AI agents a real machine. Live on October 7th.Register free

Protecting Sandbox previews with Buddy login

In the previous guide we showed how every branch gets its own preview with a public address. Anyone who gets that link can open it. For an internal tool that is convenient. That may be fine for an internal tool, but not for a client application or a preview containing sensitive data.

A sandbox endpoint can be protected with a Buddy account. Enable it with one flag, and users who are not signed in see the Buddy login screen instead of the app. No changes to the application code are required:

bash
bdy sb ep add preview-dispatch-eta -e 3300 -n www -r us --buddy $

Below we add Buddy login to the preview-up pipeline from the previous guide, restrict access to specific workspace members, and show how the application can identify the signed-in user.

Prerequisites

  • A preview environment from the previous guide or any sandbox with an HTTP endpoint
  • bdy CLI after bdy login

Buddy auth on the endpoint

An HTTP endpoint in a sandbox has three protection modes:

  • NONE (public, the default),
  • BASIC (HTTP login and password),
  • BUDDY (Buddy account).

In the dashboard, switch the mode in the endpoint settings and save with Update endpoint. In the CLI, edit the sandbox YAML: download it to a file, add auth_type: BUDDY to the endpoint, and update the sandbox with the modified file:

bash
bdy sb yaml preview-dispatch-eta > preview-dispatch-eta.yml $
yaml
endpoints: - name: www endpoint: "3300" http: auth_type: BUDDY
bash
bdy sb update preview-dispatch-eta @preview-dispatch-eta.yml $

The preview address stays the same, and Buddy auth takes effect as soon as you save. bdy sb ep get confirms the change. Note the last row:

$ bdy sb ep get preview-dispatch-eta www Field Value Name www Endpoint 0.0.0.0:3300 Type HTTP Region US Status Online URL https://www-preview-dispatch-eta-sandboxes-preview-envs-buddy-mar-l2p5r.us-1.buddy.app Auth type BUDDY

From now on every visit without a Buddy session is redirected to the login screen. You can see it in the response headers. curl has no cookies, so it gets a 302:

$ curl -sI https://www-preview-dispatch-eta-sandboxes-preview-envs-buddy-mar-l2p5r.us-1.buddy.app/ HTTP/2 302 location: https://app.buddy.works/tunnel/auth?redirect_url=https%3A%2F%2Fwww-preview-dispatch-eta-...&id=BrEZp9YM&agentId=8bGrJYBG&challenge=4a32...&state=j9Dj... cache-control: private, no-cache, no-store, max-age=0, must-revalidate

In the browser it is the regular Buddy login screen, and after signing in you land straight back in the app:

Image loading...Buddy login screen shown after opening the Buddy auth protected preview address in a private window

Anyone already signed in to Buddy never sees this screen and goes straight to the app.

In the sandbox Settings tab, the Tunnels section shows that the endpoint is behind a login:

Image loading...Settings tab of the preview-dispatch-eta sandbox, Tunnels section: the www row on port 3300 with HTTP Buddy protection and Online status

Add Buddy auth to the preview-up pipeline

There is no need to configure the endpoint manually for every new preview. The preview-up pipeline from the previous guide creates the sandbox from a spec, so that is where you add the http block. Only the endpoints fragment changes, the rest of the pipeline stays as it was:

yaml
# preview-up.yml (fragment of the Build the preview action spec) endpoints: - name: www endpoint: 0.0.0.0:3300 region: US http: auth_type: BUDDY

Update the pipeline from the file:

bash
bdy pip update preview-up @preview-up.yml -p sandboxes-preview-envs $

Every next push brings up a preview that is already closed. The link on Slack is the same as before, only now it opens for people with a Buddy account:

Image loading...Run of the preview-up pipeline for the who-is-watching branch, the Build the preview action with the BUDDY_SANDBOX_URLS output

Info

The www: 0.0.0.0:3300 shorthand does not support additional endpoint options.

The short endpoint form used in app-base.yml does not support the http block. To configure authentication, a whitelist, or custom headers, use the full endpoint form with name, endpoint, and region. The syntax of all fields is in YAML for sandboxes.

Restrict access with Permissions

Buddy auth requires the user to sign in with an account from your workspace. Access to the sandbox itself is controlled separately in the Permissions tab, including who can open the terminal, read logs, or change settings. By default the project role applies, and the Others row then shows Project role.

Image loading...Permissions tab of the preview sandbox: Member, Agent and Group sections, the Administrators & Project Managers row with the Manage role and the Others row set to None (Deny)

A proven setup for a client preview: Others set to None (Deny), and the people who should see the environment added one by one in the Member section with the View-only role. The + button opens the list of workspace members, you pick a role per row and confirm with Assign roles:

Image loading...Assign members dialog: a list of workspace members with a role picker per row, one member set to View-only, the Assign roles button at the bottom

The same setting goes into the sandbox YAML. The values differ from the UI labels: Manage is READ_WRITE, View-only is READ_ONLY, Project role is DEFAULT, and None (Deny) is DENIED:

yaml
# preview-dispatch-eta.yml (fragment) endpoints: - name: www endpoint: 0.0.0.0:3300 region: US http: auth_type: BUDDY permissions: others: DENIED
bash
bdy sb update preview-dispatch-eta @preview-dispatch-eta.yml -p sandboxes-preview-envs $
Warning

The pipeline will not set permissions for you.

The permissions block in the spec of the SANDBOX_CREATE action is silently ignored, so the sandbox is created with Others set to Project role. The pipeline applies the endpoint authentication (http.auth_type), but sandbox permissions must be configured separately in the Permissions tab or with bdy sb update and a YAML file. Workspace owners and administrators always have Manage access, and this cannot be lowered.

Info

Two other protections on the same endpoint.

For people outside the workspace, for example a client without a Buddy account, there is BASIC: bdy sb ep add preview-dispatch-eta -e 3300 -n www -r us -a review:secretpassword. Access from specific IP addresses, for example the office or a VPN, is handled by --whitelist 203.0.113.0/24. A whitelist can be combined with Buddy auth. Details in Endpoints and Permissions and security.

Identify the signed-in user

For protecting the preview alone, the above is enough. When the app needs to do something with the identity of the person on the other side, for example sign a review comment or show a panel only to admins, you reach for Identity. The tunnel in front of the sandbox serves GET /.buddy/auth/me and returns the data of the signed-in user:

$ curl -s -b "$BUDDY_COOKIES" https://www-preview-who-is-watching-.../.buddy/auth/me {"id":8635,"name":"Lucas","email":"lucas.czulak@buddy.works","owner":false,"admin":true,"avatar":null,"sso_id":null,"groups":[]}

In Node, the @buddy-works/identity library calls this endpoint and exposes the signed-in user as req.buddyUser:

bash
npm install @buddy-works/identity $
js
// server.js (fragment) const { buddyIdentity } = require('@buddy-works/identity'); const { mockAuthEndpoint } = require('@buddy-works/identity/dev'); if (process.env.NODE_ENV !== 'production') { app.use(mockAuthEndpoint('member')); } app.use(buddyIdentity()); app.get('/', (req, res) => { const viewer = req.buddyUser ? `${req.buddyUser.name} (${req.buddyUser.email})` : 'not signed in'; // ... // <span class="branch-viewer">viewing as ${viewer}</span> });

buddyIdentity() asks the tunnel for the user on every request and does not block traffic, because blocking is already done by Buddy auth on the endpoint. The buddyIdentity({ required: true }) variant rejects requests without a user with a 401, useful for an API that someone could call bypassing the browser. mockAuthEndpoint('member') injects a test user (Max Member, max@example.com) on your local machine, where there is no tunnel. In the sandbox this code does not run, because npm ci --omit=dev from fetch starts the app in production mode.

Pushing the who-is-watching branch brings up a preview through the same pipeline. The banner now shows the signed-in user:

Image loading...Dispatch Board on the who-is-watching branch preview, the banner reads viewing as Lucas (lucas.czulak@buddy.works) next to commit ca7b4de

The req.buddyUser object has the fields id, name, email, owner, admin, avatar, sso_id and groups, so without a user table of your own you can tell an admin from a regular member and check whether someone is in the QA group. In other stacks you do the same with a single GET /.buddy/auth/me, forwarding the request cookies. Any status other than 2xx means nobody is signed in.

Takeaways

  • Protection is one flag, not a feature in the code. --buddy in the CLI or http.auth_type: BUDDY in YAML and the preview closes behind a Buddy account. The app does not know anything changed.
  • Buddy auth and Permissions are two different things. The first controls who gets to the preview address. The second, who can touch the machine itself in Buddy. The pipeline sets only the first, Permissions you add separately.
  • Add Identity when the app has to do something with the identity. For simply closing the preview it is not needed. When it is, it is one library and one middleware, and locally it runs on a mock.

Details are in the docs: Endpoints, Permissions and security and Identity, and the syntax of the http and permissions blocks in YAML for sandboxes. A sandbox does not have to be just a place people look at. It can also run code you do not want to run on your own server, for example code written by an AI agent.

Jarek Dylewski

Jarek Dylewski

Customer Support

A journalist and an SEO specialist trying to find himself in the unforgiving world of coders. Gamer, a non-fiction literature fan and obsessive carnivore. Jarek uses his talents to convert the programming lingo into a cohesive and approachable narration.

Sep 29, 2026
Share