# The dev tunnel

Give an http bot a public address while you work on it locally. Covers installing cloudflared, the quick tunnel seedcord dev opens by default, using a URL you already serve, turning the tunnel off, and what happens when it fails.

Discord delivers interactions by posting them to a public URL. Your laptop doesn't have one, so Discord can't reach `localhost:3000` on your machine. Without a tunnel you'd deploy every change to a server, or set up port forwarding and paste a new endpoint into the developer portal each time. `seedcord dev` gives your http bot a public address and writes it into your Discord application for you.

> **Http only**
>
> A gateway bot opens its own connection to Discord, so it ignores the `tunnel` setting.

## What cloudflared does

`cloudflared` is a small program from Cloudflare. You run it on your machine. It opens a connection to Cloudflare, and Cloudflare gives you a public https address. Anything posted to that address comes back through the same connection to your local port.

Your router and firewall stay as they are, since your machine only makes outgoing connections.

You install `cloudflared` once. After that, `seedcord dev` starts it, reads the address, and stops it again when you quit.

## Installing it

{/* prettier-ignore-start */}

| platform | how to install                                                           |
| -------- | ------------------------------------------------------------------------ |
| macOS    | `brew install cloudflared`                                               |
| Windows  | `winget install -e --id Cloudflare.cloudflared`                          |
| Linux    | Cloudflare's package repository, a `.deb`, an `.rpm`, or a direct binary |

{/* prettier-ignore-end */}

[Cloudflare's downloads page](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/) lists every option, including Docker. seedcord looks for `cloudflared` on your `PATH`, so any install that puts it there works.

Without `cloudflared` your bot still runs. The terminal prints an install hint, and `seedcord dev` continues without a public address.

## The default, a quick tunnel

A quick tunnel works without a Cloudflare account or a domain, so leaving `tunnel` unset gives you one on every run. Writing the endpoint uses your bot's token, which seedcord reads from `DISCORD_BOT_TOKEN`.

Each start follows these steps.

1. Your bot's interactions server starts listening on its port.
2. seedcord starts `cloudflared` pointed at that port, then waits up to 60 seconds for Cloudflare to assign a hostname.
3. seedcord waits four more seconds, because Cloudflare publishes the DNS record a couple of seconds after it assigns the hostname. Discord's resolver keeps a failed lookup for about half an hour, so an early check would burn that hostname for Discord while the address answered fine from your machine.
4. seedcord posts an unsigned request to the new address until it gets a `401`. After 60 seconds seedcord continues either way. Your bot rejects an unsigned request with exactly that status, so a `401` means the tunnel reaches your port.
5. seedcord writes the address into your Discord application as its interactions endpoint.

<Image src="/tunnel-connecting.webp" alt="The dev terminal ticking off each tunnel step as it finishes" width="1452" height="616" />

Cloudflare assigns a fresh hostname every run, so the last step writes a new endpoint each time. When you quit, seedcord empties the interactions endpoint field on your Discord application, since that hostname stops working once the tunnel closes.

## When you already have a URL

A quick tunnel changes your endpoint on every run, and each run takes a few seconds to set up. When you want an address that stays the same, give `tunnel` an https URL you already serve. A named cloudflared tunnel, a tailscale funnel, a reverse proxy, or a staging server all work.

```ts title="seedcord.config.ts"
import { defineConfig } from 'seedcord';

export default defineConfig({
    root: './src',
    instance: './bot.ts',
    entry: './index.ts',
    tunnel: 'https://bot.example.com'
});
```

seedcord doesn't start `cloudflared` for `https://bot.example.com`, so you point that address at your bot's port yourself.

seedcord runs the same `401` check, then writes the endpoint only when your application holds a different value. Your address keeps working between runs, so quitting leaves that field on your application as it is.

## Turning it off

Set `tunnel` to `false` to skip all of it. Use this when you set the endpoint yourself in the developer portal, or while you work on a part of your bot that runs without a click, like a migration, a subscriber, or the shape of a card.

## When it fails

Your bot keeps running through each of these, and the error goes to the [`cli` channel](/tooling/logging).

{/* prettier-ignore-start */}

| what happened                                                | the error in your log                                 |
| ------------------------------------------------------------ | ----------------------------------------------------- |
| cloudflared didn't report a hostname within 60 seconds       | `CliTunnelUrlUnavailable`                             |
| your own URL didn't answer the `401` check within 60 seconds | `CliTunnelUnreachable`                                |
| Discord refused the address                                  | `CliTunnelNotVerified`                                |
| cloudflared exited after going live                          | cloudflared exited, so the bot has no public endpoint |

{/* prettier-ignore-end */}

The last row also turns the tunnel's status in the sidebar to `lost`.

`CliTunnelNotVerified` usually has one cause. Discord checks an endpoint by posting its own signed ping to it, and that ping fails until Discord's DNS resolver can resolve the new hostname. Pasting the same address into the developer portal keeps failing for about half an hour, since Discord's resolver holds the failed lookup that long. Press `r` to restart, which gets you a fresh hostname and another attempt.
