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

# Local Development with Tunnels

## Why you need a tunnel

Channel integrations require **public HTTPS URLs** to:

* Receive webhook events (incoming messages, postbacks, read receipts)
* Complete OAuth redirect flows
* Serve media files that external platforms download when your bot sends attachments

During local development your server runs on `localhost`, which is not reachable from the internet. A local tunnel creates a public HTTPS URL and forwards traffic back to your machine.

***

## How it works

```text theme={null}
Webhook from Facebook / WhatsApp / Telegram / ... 
		|
        |  HTTPS POST /integrations/<channel>/webhook
        ↓
  ngrok tunnel (public)
        |
        ↓
  builder app  →  localhost:3123
```

Because file URLs are generated directly from `NEXT_PUBLIC_ASSET_URL` (pointing to the RustFS object storage), external platforms also need to download files from your local RustFS instance. That requires a **second tunnel** on port `9000`.

```text theme={null}
Facebook / WhatsApp / ... (downloading an image)
		|
        |  GET https://<rustfs-tunnel>/chatbotx/<file>
        ↓
  ngrok tunnel (port 9000)
        |
        ↓
  RustFS  →  localhost:9000
```

***

## Prerequisites

* **Node.js** >= 24 and **pnpm** 10.x installed
* Docker services running (`docker compose up -d`)
* An [ngrok](https://ngrok.com) account (the free tier is sufficient)
* Credentials configured for the channel you are testing (e.g. App ID and App Secret for Messenger, WhatsApp, etc.)

***

## Step 1 — Install ngrok

<CodeGroup>
  ```bash macOS (Homebrew) theme={null}
  brew install ngrok/ngrok/ngrok
  ```

  ```bash npm theme={null}
  npm install -g ngrok
  ```

  ```bash Manual theme={null}
  # Download from https://ngrok.com/download for your OS
  ```
</CodeGroup>

Authenticate with your token:

```bash theme={null}
ngrok config add-authtoken <YOUR_NGROK_TOKEN>
```

<Info>
  Your token is on the [ngrok Dashboard → Your Auth Token](https://dashboard.ngrok.com/get-started/your-authtoken) page.
</Info>

***

## Step 2 — Start the local servers

In separate terminals, start both the builder app and make sure Docker (RustFS) is up:

```bash theme={null}
# Terminal 1 — builder app
pnpm --filter builder dev

# Terminal 2 — Docker services (PostgreSQL, Redis, RustFS, …)
docker compose up -d
```

Verify before continuing:

* Builder: `http://localhost:3123`
* RustFS: `http://localhost:9000`

***

## Step 3 — Start two ngrok tunnels

You need one tunnel for the **builder app** and one for **RustFS** (file storage).

<CodeGroup>
  ```bash Tunnel 1 — Builder app theme={null}
  ngrok http 3123
  ```

  ```bash Tunnel 2 — RustFS (file storage) theme={null}
  ngrok http 9000
  ```
</CodeGroup>

Each command will print a forwarding URL. Note both of them:

```text theme={null}
Forwarding  https://abcd-1111.ngrok-free.app -> http://localhost:3123  ← builder
Forwarding  https://efgh-2222.ngrok-free.app -> http://localhost:9000  ← RustFS
```

<Warning>
  With a free ngrok account, tunnel URLs **change every time** you restart ngrok. You must update all configuration (env vars and channel app settings) each time. To avoid this, enable a [static domain](https://ngrok.com/docs/custom-domains) (one free static domain is included in the free tier) or use a paid plan.
</Warning>

***

## Step 4 — The three URLs to configure

Replace the example hostnames with your actual tunnel URLs.

### Webhook URL

```text theme={null}
https://abcd-1111.ngrok-free.app/integrations/<channel>/webhook
```

The `<channel>` segment matches the integration name, for example:

| Channel            | Webhook URL                       |
| ------------------ | --------------------------------- |
| Facebook Messenger | `/integrations/messenger/webhook` |
| WhatsApp           | `/integrations/whatsapp/webhook`  |
| Instagram          | `/integrations/instagram/webhook` |
| Telegram           | `/integrations/telegram/webhook`  |
| Zalo               | `/integrations/zalo/webhook`      |

The route is **public** (no authentication required). The platform will:

* Call `GET <webhook-url>` with a verify token to confirm ownership
* Call `POST <webhook-url>` to deliver events (messages, postbacks, reads, …)

### OAuth Redirect URI

```text theme={null}
https://abcd-1111.ngrok-free.app/integrations/<channel>/callback
```

Register this URI in your channel app's **Valid OAuth Redirect URIs** setting so the platform accepts redirects back to your local environment after the user authenticates.

### File URL

```text theme={null}
https://efgh-2222.ngrok-free.app/chatbotx/
```

This is the public root of the RustFS bucket that the builder uses to serve uploaded files. When your bot sends an image or attachment, the platform's servers will download it from this URL.

***

## Step 5 — Update environment variables

Open the `.env` file at the project root and update these variables:

```env .env theme={null}
# ── Builder app ──────────────────────────────────────────────
BETTER_AUTH_URL=https://abcd-1111.ngrok-free.app
NEXT_PUBLIC_BUILDER_URL=https://abcd-1111.ngrok-free.app

# ── RustFS / file storage ────────────────────────────────────
# Public URL used to build asset URLs sent to external platforms
NEXT_PUBLIC_ASSET_URL=https://efgh-2222.ngrok-free.app/chatbotx/

# Internal S3 endpoint — keep pointing to localhost (server-side uploads)
S3_ENPOINT=http://localhost:9000
```

<Warning>
  After changing environment variables, **restart the builder app** (`Ctrl+C` then `pnpm --filter builder dev`) for the changes to take effect.
</Warning>

***

## Step 6 — Configure your channel app

The exact steps differ per platform, but the pattern is the same.

### Register the Webhook URL

Go to your channel's developer console and add the webhook:

* **Callback URL / Webhook URL**: `https://abcd-1111.ngrok-free.app/integrations/<channel>/webhook`
* **Verify Token**: the value you set in Organization Settings for that channel

After saving, the platform will call `GET <webhook-url>` to verify. If the server responds correctly the webhook is active.

Subscribe to the event types you need (e.g. `messages`, `messaging_postbacks`, `message_reads` for Messenger).

### Register the OAuth Redirect URI

In your channel app's **Login / OAuth** settings, add:

```text theme={null}
https://abcd-1111.ngrok-free.app/integrations/<channel>/callback
```

### Configure Organization Settings in the app

Go to **Organization Settings → \[Channel Name]** and fill in your App ID, App Secret, API version, and Verify Token. These values come from your channel developer console.

***

## Step 7 — Verify the setup

**Check webhook verification**

Most platforms send a `GET` request to the webhook URL when you save it. If the response is `200 OK` with the challenge value, the webhook is verified.

You can also test manually:

```bash theme={null}
# Replace values with your tunnel URL, channel, and verify token
curl "https://abcd-1111.ngrok-free.app/integrations/messenger/webhook\
?hub.mode=subscribe\
&hub.verify_token=YOUR_VERIFY_TOKEN\
&hub.challenge=hello"
# Expected response: hello
```

**Monitor live traffic**

ngrok's web interface at `http://127.0.0.1:4040` shows all requests and responses in real time — useful for debugging webhook payloads.

**Send a test message**

Send a message to your bot via the connected channel. You should see:

* A `POST /integrations/<channel>/webhook` request in the ngrok dashboard
* The message appear in the inbox inside the app

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook verification fails">
    * Make sure the `verifyToken` in Organization Settings exactly matches what you registered in the channel app
    * Confirm that the builder is running and the tunnel is active
    * Check the ngrok dashboard (`http://127.0.0.1:4040`) to see the raw request and response
  </Accordion>

  <Accordion title="No POST events received after verification">
    * Verify you have subscribed to the required event types in the channel app
    * Confirm the tunnel is still running (free ngrok tunnels time out after a period of inactivity on the free plan)
    * Check that `NEXT_PUBLIC_BUILDER_URL` and `BETTER_AUTH_URL` match the current tunnel URL exactly
  </Accordion>

  <Accordion title="Media files return 403 or cannot be downloaded by the platform">
    * Check that `NEXT_PUBLIC_ASSET_URL` in `.env` points to the **RustFS tunnel URL** (not `localhost`)
    * Make sure you restarted the builder after updating the env variable
    * Verify the RustFS tunnel is running: `curl https://efgh-2222.ngrok-free.app/chatbotx/` should return a response from RustFS (even an error is fine — a connection refusal means the tunnel is down)
    * Ensure the Docker `filesystem` service is healthy: `docker compose ps filesystem`
  </Accordion>

  <Accordion title="Tunnel URL changed after restarting ngrok">
    This is expected on the free plan. After each restart:

    1. Note the new tunnel URLs from the ngrok terminal output
    2. Update `BETTER_AUTH_URL`, `NEXT_PUBLIC_BUILDER_URL`, and `NEXT_PUBLIC_ASSET_URL` in `.env`
    3. Restart the builder app
    4. Update the Webhook URL and OAuth Redirect URI in the channel developer console

    To avoid repeating this, enable a [ngrok static domain](https://ngrok.com/docs/ngrok-agent/config#tunnels) — one is included for free.
  </Accordion>
</AccordionGroup>

***

## Alternative tunnel tools

If you prefer not to use ngrok, the following tools work the same way:

| Tool                                                                                                                                   | Command                                          | Notes                     |
| -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------- |
| [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) | `cloudflared tunnel --url http://localhost:3123` | Free, random URL each run |
| [localtunnel](https://localtunnel.github.io/www/)                                                                                      | `npx localtunnel --port 3123`                    | Free, no account needed   |
| [Expose](https://expose.dev/)                                                                                                          | `expose share http://localhost:3123`             | Free and paid plans       |

Run a second instance of each on port `9000` for the RustFS tunnel. All subsequent configuration steps are identical — just substitute the tunnel URLs accordingly.
