# Self-hosting the sync server (/en/docs/self-hosting)



The sync backend (`oncewise-ai-sync`) is a Rust/Axum service over PostgreSQL. It stores and distributes structured script data for [spaces](./sync) and never generates or executes flows. The server keeps no accounts, member tables or user profiles of its own: key spaces have no ownership, and on a gated deployment (below) the membership and roles of org-bound spaces live in the Herald organization layer, with the server storing only the space-to-organization link.

## Quick run from source [#quick-run-from-source]

For trying things out or developing:

```bash
docker run -d --name oncewise-demo-pg -e POSTGRES_PASSWORD=postgres -p 127.0.0.1:5432:5432 postgres:18-alpine

cd backend
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres AUTH_MODE=none cargo run
```

The server listens on `0.0.0.0:8080` by default and runs database migrations at startup. Verify with:

```bash
curl http://127.0.0.1:8080/api/health        # → 200
curl http://127.0.0.1:8080/api/openapi.json  # the OpenAPI 3 document
```

`AUTH_MODE=none` is used above because the default mode requires Herald configuration (below). For anything reachable beyond your machine, do not ship `none`.

## Container image [#container-image]

Every `v*.*.*` tag publishes an image:

```bash
docker pull ghcr.io/timzaak/oncewise:latest
docker run -d --name oncewise-sync \
  -e DATABASE_URL=postgres://user:pass@db:5432/oncewise \
  -e AUTH_MODE=none \
  -p 8080:8080 \
  ghcr.io/timzaak/oncewise:latest
```

Point the extension's Sync tab at the server's origin (for example `https://sync.example.com`) and you are done on the client side.

## Environment variables [#environment-variables]

| Variable              | Default        | Required      | Notes                                                                                                                                                                           |
| --------------------- | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATABASE_URL`        | —              | yes           | `postgres://user:pass@host:5432/dbname`                                                                                                                                         |
| `BIND_ADDR`           | `0.0.0.0:8080` | no            | Listen address                                                                                                                                                                  |
| `AUTH_MODE`           | `herald`       | no            | `herald` or `none`                                                                                                                                                              |
| `HERALD_BASE_URL`     | —              | when `herald` | HTTPS origin (HTTP only on loopback), no path/query/fragment                                                                                                                    |
| `HERALD_REALM_ID`     | —              | when `herald` | The deployment-level realm; must match the status response                                                                                                                      |
| `HERALD_CLIENT_ID`    | —              | when `herald` | OAuth client app id registered in Herald                                                                                                                                        |
| `HERALD_REDIRECT_URI` | —              | when `herald` | HTTPS URL whose path is exactly `/api/auth/oauth/callback`                                                                                                                      |
| `HERALD_ORG_API_KEY`  | —              | when `herald` | Server-to-server key for the org channel: an API key bound to the `HERALD_CLIENT_ID` client app, carrying a role with the `orgs.view` and `orgs.manage` policies (1–4096 chars) |

Startup fails fast with the names (never the values) of whatever is missing or malformed, so a broken config is safe to paste into an issue. Prepare `HERALD_ORG_API_KEY` in the Herald console: reuse (or create) the client app named by `HERALD_CLIENT_ID`, issue an API key bound to that app, and assign the key a role carrying `orgs.view` and `orgs.manage`. The startup error prints these steps when the variable is missing.

## The Herald sign-in gate [#the-herald-sign-in-gate]

By default (`AUTH_MODE=herald`) the server puts its business endpoints behind a sign-in: clients obtain a bearer token through the server's OAuth flow against a [Herald](https://github.com/timzaak/herald) deployment (also open source; its image is `ghcr.io/timzaak/herald`). Health, the OpenAPI document and the auth endpoints stay public.

The sign-in identity also powers the workspace layer: spaces created on a gated deployment are org-bound — the creator is the owner, joining happens by one-time invite, and membership with roles is resolved live from Herald on every request, so a removal or demotion takes effect on that person's next access. Spaces that predate the upgrade keep the space-code model with unchanged semantics; nothing is migrated.

On the extension side this is the login card in the Sync tab: one authorization window, then silent renewal.

`AUTH_MODE=none` restores the space-key-only behavior explicitly: every space is key-addressed, and the workspace endpoints (invites, members, ownership) answer 404. Anyone who obtains a space code can then use your server's space endpoints against it, so treat `none` as a loopback/dev convenience, not a deployment posture. Setting Herald variables while `AUTH_MODE=none` is active logs that they are being ignored.

## HTTP surface [#http-surface]

Endpoints group into `auth` (sign-in mode probe, OAuth start/callback, token refresh), `spaces`, a workspace group (invite issue and redeem, member list, role change, removal and self-exit, ownership transfer — gated deployments only), `scripts` and `ops` (health, OpenAPI). Key-space requests authenticate with the `X-Space-Key` header; gated deployments also require the bearer token in front of it. Org-bound spaces send no space key at all — access is the bearer token plus membership and role, judged live from Herald. The full request/response schemas are rendered from the backend's utoipa annotations in the [OpenAPI reference](./openapi) and served live at `/api/openapi.json`.
