# Self-hosting

The agents backend is a single Go binary. Run it in your own cloud, point the SDKs at it, and nothing about an agent changes: the same `agent.yaml` syncs to it, the same sessions open on it, the same client SDKs talk to it. This page covers the YAML it reads, how callers prove who they are, and how to move the data you already have into it without a window where writes are lost.

## What you run

| Piece                                  | Why                                           | Without it                                                   |
| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ |
| The router (`acceleration/cmd/router`) | Serves the API, routes models, holds sessions | Nothing                                                      |
| Postgres                               | Agents, conversations, keys, usage            | The router routes but records nothing, and `/health` says so |
| Redis                                  | Live provider health and the daily limits     | Routing ignores which providers are struggling               |
| An object bucket                       | Recordings behind a voice of your own         | Voices of your own are unavailable                           |

Provider credentials stay in the environment, one variable per provider, and a provider whose credential is missing is skipped rather than fatal. So a router with only `OPENAI_API_KEY` set routes to OpenAI and to nothing else.

The quickest way to see all of it running is the compose file in the repository, which starts Postgres, Redis, the router and the dashboard together:

```bash
git clone https://github.com/GetStream/Vision-Agents
cd Vision-Agents
docker compose up --build
```

On your own machines, the binary is enough. It runs its own migrations on start, so there is no separate step before the first boot:

```bash
go build -o router ./acceleration/cmd/router
./router --config /etc/router.yaml
```

## Your YAML

Everything that is not a provider credential is one YAML file, and `--config` says which:

```yaml title="/etc/router.yaml"
addr: ":8080"
# Where callers reach you, which behind a load balancer is not where you listen.
# Telephony vendors fetch a call plan from it on answer.
public_url: https://agents.example.com
log_level: info

postgres:
  dsn: postgres://agents:secret@db.internal:5432/agents?sslmode=require

redis:
  addr: cache.internal:6379
  username: agents
  password: secret

auth:
  mode: api_key
  # Unseals the stored key secrets. Keep it out of this file: pass it as
  # ROUTER_AUTH_KEK, from whatever holds your secrets.
  kek: ""

# Browser origins allowed to call the API and open sockets. Empty means none,
# which is right unless a browser app talks to this deployment directly.
cors_origins:
  - https://app.example.com
# CIDR ranges your own proxies sit in. Decides how much of X-Forwarded-For is
# believed. Empty means none of it is.
trusted_proxies:
  - 10.0.0.0/8

# The default for what one end user may spend in a UTC day. An app sets its
# own with `quota` in its policy. A 0 is no default cap on that half, and an
# app's own `quota` still applies.
rate_limit:
  messages_per_day: 200
  tokens_per_day: 5000000

# How long recorded changes are kept while somebody is moving their data here.
data_move:
  retention: 168h

# The Stream app whose secret signs call events and the tokens a browser joins
# a call with. Only needed for voice on Stream Video.
stream:
  api_key: your_stream_key
  api_secret: your_stream_secret

# Paths to capability files. Empty means the catalogue built into the binary.
routing_config: ""
phone_config: ""
voices_bucket_url: s3://voices?region=eu-west-1
```

`rate_limit` is only the default. An app sets its own with `quota`: see [Daily limits](https://getstream.io/agents/docs/go-golang/router-intro/#daily-limits).

`postgres.dsn` and `redis.addr` are the two a self-hosted deployment always sets. An empty `postgres.dsn` is a router that records nothing, which is fine for a smoke test and not for anything else.

Three ways to choose the file, in the order they win:

| How                                   | What it does                                                              |
| ------------------------------------- | ------------------------------------------------------------------------- |
| `--config /etc/router.yaml`           | The file this deployment runs on                                          |
| `ROUTER_CONFIG_FILE=/etc/router.yaml` | The same thing, for a chart that sets environment and not arguments       |
| `ROUTER_ENV=local\|testing\|staging`  | One of the files built into the binary, when you name no file of your own |

Saying nothing at all picks `local`, which points at the Postgres and Redis that the compose file starts.

Every setting also has the environment variable it has always had, and the variable wins over the file. `ROUTER_POSTGRES_DSN`, `ROUTER_REDIS_ADDR` and `ROUTER_AUTH_KEK` are the three worth using that way, because a DSN and a key encryption key are secrets and a file checked in beside your charts is not where a secret belongs.

## Authentication

`auth.mode` picks between four deployments:

| Mode      | Who a caller is                                                                                         | Use it when                                           |
| --------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `api_key` | An API key, plus a token signed with the secret behind it. The default                                  | The router is reachable by anything you did not write |
| `proxy`   | Whatever the thing in front of the router already decided, read from headers it overwrites              | You have a gateway that authenticates                 |
| `noauth`  | The customer named in `X-Customer-Id`, taken at face value, and every caller is that customer's backend | A laptop                                              |
| `custom`  | An authenticator you supply in code                                                                     | You have an answer of your own                        |

The default verifies, on purpose. A default that trusts whatever reaches it is only correct when something else guarantees that nothing does, and that guarantee is a NetworkPolicy in one deployment and nothing at all in the next. `api_key` refuses to start without a database and `auth.kek`, rather than refusing every request later.

>
> **Warning:** `noauth` means the tenant is whatever the caller writes in a header. Anything that can open a TCP connection to the router is every customer on it. Keep it on a laptop.
>

### The first credential

`api_key` mode cannot bootstrap itself over HTTP: every endpoint wants a key and there is nobody to issue the first one. A command run beside the database can:

```bash
ROUTER_AUTH_KEK=$KEK ./router --config /etc/router.yaml keys create \
  --org "Acme" --app-name "Acme production" --key-name default
```

```text
organization  01JC...
app           01JC...
key           vak_live_7f2b...
secret        vas_live_9a41...

The secret is shown once. Set it where your SDK reads it and keep it out of the router's own config.
```

The secret is printed and never logged or shown again. The app id it prints is the tenant: it is what every row is keyed by. If you are moving off another deployment, pass the app id you had there with `--app-id`, so the rows you import still belong to something:

```bash
./router keys create --app-id 1234567 --key-name moved
```

Running it again for an app that exists adds a second key rather than refusing, which is how you rotate.

### What a device gets

A device never holds the secret. Your backend mints a token for the user, the device sends the key and that token, and the router marks it as a device: it may open and hold its own conversations, and everything that configures an agent answers it with a 403. That split is the same self-hosted as hosted.

## Point your SDK at it

Server-side SDKs take the router's URL and a key and secret. Use the ones `keys create` printed, not your Stream app's.

**Environment**

```bash
export STREAM_ACCELERATION_URL=https://agents.example.com
export STREAM_API_KEY=vak_live_7f2b...
export STREAM_API_SECRET=vas_live_9a41...
```

Leave `STREAM_ACCELERATION_AUTHENTICATE` unset. It says the router is behind Stream's own authenticating proxy, which a deployment of yours is not.

The Go SDK is the exception: it reads these variables only for the proxy, and names a tenant with `STREAM_ACCELERATION_CUSTOMER_ID` otherwise.

**Python**

```python
from vision_agents.plugins import stream

api = stream.Client(
    url="https://agents.example.com",
    api_key="vak_live_7f2b...",
    api_secret="vas_live_9a41...",
)
```

**Node**

```ts
import { Client } from "@stream-io/calypso";

const api = new Client({
  url: "https://agents.example.com",
  apiKey: "vak_live_7f2b...",
  apiSecret: process.env.ROUTER_API_SECRET!,
});
```

**Go**

```go
api, err := stream.NewClient(stream.Backend{
    URL:        "https://agents.example.com",
    CustomerID: "1234567",
})
```

The Go SDK names the tenant rather than carrying a key of its own, so it reaches a deployment running in `proxy` mode behind a gateway that authenticates, or in `noauth` mode on a laptop. `api_key` support is on the [roadmap](https://getstream.io/agents/docs/go-golang/changelog-roadmap/).

**.NET**

```csharp
using var api = new VisionAgentsClient(new VisionAgentsOptions
{
    Url = "https://agents.example.com",
    ApiKey = "vak_live_7f2b...",
    ApiSecret = Environment.GetEnvironmentVariable("ROUTER_API_SECRET"),
});
```

**Ruby**

```ruby
api = GetStream::VisionAgents::Client.new(
  url: "https://agents.example.com",
  api_key: "vak_live_7f2b...",
  api_secret: ENV["ROUTER_API_SECRET"]
)
```

**Rust**

```rust
let api = Client::new(ClientOptions {
    url: Some("https://agents.example.com".into()),
    api_key: Some("vak_live_7f2b...".into()),
    api_secret: std::env::var("ROUTER_API_SECRET").ok(),
    ..Default::default()
})?;
```

**PHP**

```php
$client = new Client(new Backend(
    url: 'https://agents.example.com',
    apiKey: 'vak_live_7f2b...',
    apiSecret: getenv('ROUTER_API_SECRET'),
));
```

Client SDKs only need the URL and the credentials your backend hands down. None of them holds a secret.

**JavaScript**

```ts
const api = new Client({
  url: "https://agents.example.com",
  apiKey: "vak_live_7f2b...",
  token: () => fetch("/api/agent-token").then((response) => response.text()),
});
```

Add the app's origin to `cors_origins`, or the browser never gets past the preflight.

**Kotlin**

```kotlin
val agents = VisionAgents(url = "https://agents.example.com", apiKey = "vak_live_7f2b...")
agents.setUser(User(id = "alice"), token = tokenFromYourBackend)
```

**Dart**

```dart
final agents = VisionAgents(
  url: Uri.parse('https://agents.example.com'),
  apiKey: 'vak_live_7f2b...',
  userId: 'alice',
  token: () async => tokenFromYourBackend,
);
```

**Swift**

```swift
let agents = VisionAgents(apiKey: "vak_live_7f2b...", url: URL(string: "https://agents.example.com")!)
agents.setUser(User(id: "alice"), token: tokenFromYourBackend)
```

## Move your data here

Three endpoints move a tenant between two deployments, and all three are server-side only.

| Endpoint                              | What it does                                                                                                  |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `GET /v1/data/export`                 | Everything the calling app has, as one JSON object per line, ending with the cursor its changes carry on from |
| `POST /v1/data/import`                | Writes an export, or a batch of changes, into this deployment                                                 |
| `GET /v1/data/changes?after=<cursor>` | What has happened to the app's rows since that cursor                                                         |

The customer is always the caller. Nothing in the request body or the query string can name a different one, so an export is your own data and an import lands under your own app however the file was edited on the way.

An export deliberately does not carry:

- **API key secrets.** Key rows are not exported at all. Mint new ones on the new deployment with `keys create`.
- **OAuth tokens** for connected plugins: access tokens, refresh tokens and the state behind a login in progress. A connection comes across and has to be authorized again.
- **Organizations and apps.** Those are the account, and the new deployment has its own. `keys create --app-id` is what puts the rows under the right one.

Everything else comes across: agent configs, router configs, conversations and their turns, knowledge, campaigns, phone numbers, usage. Voice recordings live in the object bucket rather than in Postgres, so the rows come across and copying the bucket is yours to do.

### Switch over with no downtime

`router replicate` does both halves: the copy, and everything that happens after the copy. Run it on the new deployment, pointed at the old one.

1. Create the app and a key on the new deployment, reusing the app id you have today:

   ```bash
   ./router --config /etc/router.yaml keys create --app-id 1234567 --key-name moved
   ```

2. Start replicating. The copy is read at one moment; the changes carry on from exactly where it stopped:

   ```bash
   ./router --config /etc/router.yaml replicate \
     --from https://accelerate-us-east4.gcp.stream-io-api.com \
     --api-key vak_live_7f2b... --api-secret vas_live_9a41...
   ```

   It prints `caught up at cursor N` once it is following live, and keeps following until you stop it.

3. Point your SDKs at the new URL and deploy. Writes that were in flight against the old deployment are still being replayed.

4. Run it once more with `--once`, which stops as soon as there is nothing left to catch up on, and you are moved.

Applying the same change twice lands where applying it once would, so a replay that overlaps the switchover is not a problem. The one thing that does not wait is `data_move.retention`, seven days by default: a move that takes longer than that starts again from a fresh export rather than resuming from a cursor the old deployment no longer honours, and asking for one returns 410.

Recording changes costs nothing until somebody is moving. The triggers return early unless an export has started, and they stop again on their own when nobody has read a page for the retention window.

### Doing it by hand

The endpoints are plain HTTP, so a move is two curl commands if you would rather:

```bash
curl -sS https://old.example.com/v1/data/export \
  -H "X-Api-Key: $OLD_KEY" -H "Stream-Auth-Type: server" \
  -H "Authorization: Bearer $OLD_SERVER_TOKEN" > export.ndjson

curl -sS -X POST https://agents.example.com/v1/data/import \
  -H "X-Api-Key: $NEW_KEY" -H "Stream-Auth-Type: server" \
  -H "Authorization: Bearer $NEW_SERVER_TOKEN" \
  --data-binary @export.ndjson
```

The server token is an HS256 JWT signed with the key's secret, carrying `server: true`, no `user_id`, and an expiry. The last line of the export names the cursor: if it is missing, the stream was cut short and the file is not a complete export.

### Who may move data

The three endpoints refuse three kinds of caller, and the refusals are worth knowing before you wonder why a request failed:

- A caller nobody authenticated gets a 401. There is no customer, so there is no data.
- A device gets a 403. A token your backend minted for a user lets them hold a conversation, not walk off with the account.
- Every caller gets a 403 while the router runs in `noauth` mode. There the customer is whatever the caller says it is, so serving an export would mean handing anybody anybody's data for the price of naming them.

## Next steps

- [Architecture](https://getstream.io/agents/docs/go-golang/sovereign-ai/): what the backend is doing with all this.
- [Quickstart](https://getstream.io/agents/docs/go-golang/): an agent answering against it.
- The [router README](https://github.com/GetStream/Vision-Agents/blob/main/acceleration/README.md) documents every provider credential and every setting in depth.

---

For the most recent version of this documentation, visit [https://getstream.io/agents/docs/go-golang/self-hosting/](https://getstream.io/agents/docs/go-golang/self-hosting/).