> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boat.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Public URLs

> Expose a port of a sandbox on a stable public HTTPS URL, protect it with a token, rename its subdomain, or open a raw TCP or UDP port.

Run a service in your sandbox. Boat gives it a stable public HTTPS URL.

## Quick path

The API examples use `BOAT_API_BASE=https://boat.dev/api/v1`.

<Steps>
  <Step title="Start your app in the background">
    Start the app as a detached process on an explicit port. A normal command stops after 30 seconds, and a dev server never ends.

    <CodeGroup>
      ```bash CLI theme={null}
      boat exec bx_f7k2q9hd --cwd my-repo --detach "npm run dev -- --host 0.0.0.0 --port 3000"
      ```

      ```bash curl theme={null}
      curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/commands" \
        -H "Authorization: Bearer $BOAT_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"command":"npm run dev -- --host 0.0.0.0 --port 3000","cwd":"my-repo","detached":true}'
      ```

      ```ts TypeScript theme={null}
      await sandbox.command({
        sandboxId: "bx_f7k2q9hd",
        command: "npm run dev -- --host 0.0.0.0 --port 3000",
        cwd: "my-repo",
        detached: true,
      });
      ```

      ```python Python theme={null}
      from boat_sdk.models.command_request import CommandRequest

      sandbox.command("bx_f7k2q9hd", CommandRequest(
          command="npm run dev -- --host 0.0.0.0 --port 3000",
          cwd="my-repo",
          detached=True,
      ))
      ```
    </CodeGroup>

    <Info>
      An app on `0.0.0.0` needs no flag. An app on `localhost`, `127.0.0.1` or `::1` needs `--localhost` in the next step (`"localhost": true` in the API). See [Services that listen on localhost](/hosting/how-it-works#services-that-listen-on-localhost).
    </Info>
  </Step>

  <Step title="Host the port">
    <CodeGroup>
      ```bash CLI theme={null}
      boat host bx_f7k2q9hd 3000 --title "App preview"
      ```

      ```bash curl theme={null}
      curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/host" \
        -H "Authorization: Bearer $BOAT_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{"port":3000,"title":"App preview"}'
      ```

      ```ts TypeScript theme={null}
      const hosted = await sandbox.hostPort({ sandboxId: "bx_f7k2q9hd", port: 3000, title: "App preview" });
      console.log(hosted.url);
      ```

      ```python Python theme={null}
      from boat_sdk.models.host_port_request import HostPortRequest

      hosted = sandbox.host_port("bx_f7k2q9hd", HostPortRequest(port=3000, title="App preview"))
      print(hosted.url)
      ```
    </CodeGroup>

    Inside the sandbox, `host 3000` does the same thing.
  </Step>

  <Step title="Open the URL">
    The call returns the URL. A new URL is protected by default and includes a `_token` query parameter:

    ```text theme={null}
    https://<sandbox-subdomain>-3000.on.boat.dev?_token=<access-token>
    ```

    Open it in a browser. To get a URL without a token, add `--public` (`"public": true` in the API).
  </Step>
</Steps>

## Who can open a protected URL

* Anyone who has the full URL with `_token` can open it.
* Boat does not ask for a Boat sign-in. Boat does not check the organization.
* Treat the full URL like a password.

The `_token` URL answers with a redirect that sets a cookie. A browser follows it. For `curl`, keep the cookie:

```bash theme={null}
curl -L -c cookies.txt -b cookies.txt "https://<sandbox-subdomain>-3000.on.boat.dev?_token=<access-token>"
```

An API client can send the token as a cookie header instead:

```bash theme={null}
curl -H "Cookie: _port_auth=<access-token>" "https://<sandbox-subdomain>-3000.on.boat.dev/api/health"
```

### Show a protected URL in an iframe

In an iframe on another site, the `_port_auth` cookie is a third-party cookie. Browsers can block it, and the frame then stays blank. The same URL works in its own tab.

* To embed a preview, host the port with `--public`.
* Or put your own server in front of the URL. Your server sends `Cookie: _port_auth=<access-token>` to Boat.

### Replace a leaked token

Boat has no rotate command. Do these two calls one after the other:

```bash theme={null}
boat host bx_f7k2q9hd 3000 --public   # removes the old token
boat host bx_f7k2q9hd 3000            # makes a new token and prints the new URL
```

* The old `_token` URL stops working.
* Between the two calls, the URL is open without a token. Stop the app first if this is a problem.
* `host hide` does not change the token. A later `host` brings back the same token.

## Facts

| Fact | Value |
| - | - |
| URL format | `https://<sandbox-subdomain>-<port>.on.boat.dev` |
| Ports per sandbox | Up to 50 |
| Default access | Protected, with `_token` |
| Address your app listens on | `0.0.0.0`, or `localhost` with `--localhost` |
| Host from your computer | `boat host <sandbox-id> <port>`, or `POST /sandboxes/{id}/host` |
| Host from inside the sandbox | `host <port>` |
| After a stop and resume | The URL stays the same. Start your app again. |
| Ports that Boat uses | 22, 6080, 8090, 8911, 47984 to 48010. Do not run your app on them. |
| Custom domain | Not supported. See [Custom domain](#custom-domain). |
| Raw TCP or UDP | Through the sandbox IP address. See [Raw TCP or UDP ports](#raw-tcp-or-udp-ports). |

## Choose the subdomain

Every sandbox has one subdomain. All its public URLs use it. To change it, update the sandbox:

<CodeGroup>
  ```bash curl theme={null}
  curl -sS -X PATCH "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"subdomain":"acme-staging"}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.update({ sandboxId: "bx_f7k2q9hd", subdomain: "acme-staging" });
  ```

  ```python Python theme={null}
  from boat_sdk.models.update_sandbox_request import UpdateSandboxRequest

  sandbox.update("bx_f7k2q9hd", UpdateSandboxRequest(subdomain="acme-staging"))
  ```
</CodeGroup>

* The CLI has no flag for this.
* Use 3 to 40 lowercase letters, digits and single hyphens.
* The name cannot end with `-desktop` or with `-<number>`.
* `409 subdomain_taken` means another sandbox uses the name.
* The sandbox must be running or fully stopped. Else the API returns `sandbox_busy`.
* Each hosted port moves to the new name, for example `acme-staging-3000.on.boat.dev`.
* The old URLs stop working. The `_token` values stay the same.

## Custom domain

Boat does not support custom domains.

* A CNAME record to an `on.boat.dev` name does not work. Boat routes and issues TLS certificates only for `on.boat.dev` names.
* An A record to the sandbox IP address breaks at the next resume, because the IP address changes.

To show the app on your own domain, run your own reverse proxy or CDN in front of the public URL. The proxy must send the `on.boat.dev` host name to Boat.

## Raw TCP or UDP ports

The public URL carries HTTPS and WebSockets only. For another protocol, open the port in the sandbox firewall and connect to the sandbox IP address.

<Steps>
  <Step title="Open the port">
    ```bash theme={null}
    boat ssh bx_f7k2q9hd "sudo ufw allow 27015/udp"
    ```

    Use `27015/tcp` for TCP.
  </Step>

  <Step title="Get the address">
    ```bash theme={null}
    boat info bx_f7k2q9hd
    ```

    Connect to `ip` on your port.
  </Step>
</Steps>

* The IP address changes at each resume. Get it again after a resume.
* Snapshots do not keep firewall rules. Open the port again after each resume and fork.
* `boat host <id> <port> --public` also opens that TCP port on the IP address. Boat opens it again after a resume.
* Some sandboxes have no public IPv4 address. Then `boat info` shows `sshEndpoint`, and `ip` is an IPv6 address. Only clients with IPv6 can reach a raw port there.

## Learn more

<Columns cols={2}>
  <Card title="Expose a port: host commands" icon="terminal" href="/hosting/host-cli">
    `boat host`, `host`, `host list`, `host url` and `host hide`, with all options.
  </Card>

  <Card title="How public URLs work" icon="globe" href="/hosting/how-it-works">
    How Boat routes a request to your sandbox.
  </Card>

  <Card title="Firewall rules" icon="shield" href="/hosting/firewall">
    Add firewall rules and block outbound traffic without breaking Boat.
  </Card>

  <Card title="Forward a port to your computer" icon="arrow-right-arrow-left" href="/ssh-access#forward-a-port">
    Reach a sandbox port on your own `localhost`, with no public URL.
  </Card>
</Columns>

## Related

* [SSH](/ssh-access)
* [Security for platforms](/security)
* [Long-running tasks](/long-running-tasks)
* [Desktop streaming](/desktop-streaming)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.