# Run Scribase in your own cluster

This page is for a deployment outside hosted Scribase, under a written
agreement (see [/enterprise](https://scribase.com/enterprise)).

Scribase ships two first-class deploy targets under `deploy/`:

| Target | Path | Use it for |
|---|---|---|
| Docker Compose | `deploy/compose/` | Single-box deployments — one host, all services |
| Helm chart | `deploy/helm/scribase/` | Kubernetes — control plane and optional shared data plane |

Hosting many small projects (one per app) on one VPS? Use the
[single-box layout](https://docs.scribase.com/docs/single-box.md) instead: scale-to-zero project containers,
shared storage, realtime and functions, and built-in admin sign-in.

The service stack is the same in both: **postgres · control-api · gateway (Envoy) · auth · rest · storage · realtime · functions · worker**. Background jobs (backups, restores, verifications, exports, scheduled backups, retention, export expiry and usage metering) run inside `control-api` as its data-job workers, so there is no separate scheduler service.

---

## What you must provide

Everything else has a working default. These four are yours to supply:

1. **A PostgreSQL 17 database.** Compose runs one for you (`supabase/postgres`); a production install points at your own managed database. The schema migrator needs a role with `BYPASSRLS`; the API and operator use least-privilege login roles.
2. **A public domain** — where clients reach the gateway, e.g. `scribase.example.com`.
3. **A TLS certificate** for that domain. The gateway speaks plain HTTP internally and trusts a TLS terminator in front of it.
4. **Operator sign-in** for the management API: either the control plane's own password login (`SCRIBASE_AUTH_MODE=builtin`, no identity provider needed; see [Built-in admin login](https://docs.scribase.com/docs/admin-login.md)), or an OIDC provider with token introspection (`SCRIBASE_AUTH_MODE=oidc` + `SCRIBASE_OIDC_*`). If you provision managed per-tenant databases via Neon, also a **Neon API key** (`SCRIBASE_NEON_API_KEY`).

Secrets are never baked into images or committed. Generate them, then pass them via `.env` (Compose) or a Kubernetes Secret / external secret store (Helm).

---

## Single-box deploy (Docker Compose)

Files: `deploy/compose/docker-compose.yml`, `.env.example`, `envoy/envoy.yaml`, `initdb/`.

### 1. Configure

```sh
cd scribase/deploy/compose
cp .env.example .env
# Edit .env: set POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY,
# SECRET_KEY_BASE, and either SCRIBASE_AUTH_MODE=builtin or the
# SCRIBASE_OIDC_* values. See the file comments.
```

Required secrets to generate:

```sh
# JWT secret (at least 32 random bytes, base64-encoded)
openssl rand -base64 32

# Anon key and service-role key are JWTs signed with JWT_SECRET.
# Generate them with the scribase CLI or the Scribase Console.
```

### 2. Validate the config (no daemon required)

```sh
docker compose --env-file .env -f docker-compose.yml config
```

### 3. Boot

```sh
docker compose --env-file .env -f docker-compose.yml up -d --build
```

Boot order is enforced by health and completion gates:

1. `db` becomes healthy
2. `migrate` (one-shot) applies `postgres/migrations/*.sql` and exits `0`
3. `control-api`, `auth`, `rest`, `storage`, `realtime`, `functions` start
4. `gateway` (Envoy) starts once all data-plane services are up

### 4. Verify

```sh
# Public API through the gateway
curl http://localhost:8000/health/ready

# Management API directly
curl http://localhost:8081/health/ready
```

### 5. Register your first organization

```sh
export SCRIBASE_API_URL=http://localhost:8081
# builtin mode: the first run creates the administrator and signs in;
# later runs use `scribase login --email you@example.com`.
scribase admin setup --email you@example.com --setup-token scb_setup_...
# oidc mode instead: export SCRIBASE_ACCESS_TOKEN=<token from your identity provider>

scribase doctor
scribase org put myorg "My Organization"
scribase project put myorg myproject aws-us-east-1
scribase env create myorg myproject production production snapshot aws-us-east-1 init-001
scribase operation get myorg <operation-id>
```

---

## Kubernetes deploy (Helm)

The Helm chart at `deploy/helm/scribase/` manages the control-plane components and its PostgreSQL dependency.

### Prerequisites

- Helm 3.x
- A Kubernetes cluster (1.28+)
- A PostgreSQL 17 database accessible from the cluster
- A TLS-terminating Ingress controller

### 1. Add values

```sh
cp deploy/helm/scribase/values.yaml myvalues.yaml
# Edit myvalues.yaml
```

Key values to set:

```yaml
controlApi:
  database:
    url: "postgresql://scribase_api:PASSWORD@postgres:5432/scribase?sslmode=require"
  oidc:
    introspectionEndpoint: "https://idp.example.com/oauth/introspect"
    issuer: "https://idp.example.com"
    audience: "scribase-api"
    clientId: "scribase-control"
    clientSecret: "SECRET"

gateway:
  domain: "scribase.example.com"

secrets:
  jwtSecret: "GENERATED_JWT_SECRET"
  anonKey: "GENERATED_ANON_JWT"
  serviceRoleKey: "GENERATED_SERVICE_ROLE_JWT"
```

### 2. Install

```sh
helm install scribase deploy/helm/scribase/ -f myvalues.yaml -n scribase --create-namespace
```

### 3. Upgrade

```sh
helm upgrade scribase deploy/helm/scribase/ -f myvalues.yaml -n scribase
```

### 4. Scale

```sh
kubectl scale deployment scribase-control-api --replicas=3 -n scribase
```

---

## Environment variables reference

### Control API

| Variable | Required | Description |
|---|---|---|
| `SCRIBASE_API_DATABASE_URL` | Yes | TLS URL for the `scribase_api` role |
| `SCRIBASE_AUTH_MODE` | No | `oidc` (default here) or `builtin` |
| `SCRIBASE_ADMIN_EMAIL`, `SCRIBASE_ADMIN_PASSWORD_HASH` | No | builtin only: pre-seed the first administrator |
| `SCRIBASE_OIDC_INTROSPECTION_ENDPOINT` | oidc only | RFC 7662 token introspection HTTPS endpoint |
| `SCRIBASE_OIDC_ISSUER` | oidc only | Exact trusted `iss` claim |
| `SCRIBASE_OIDC_AUDIENCE` | oidc only | Required `aud` claim |
| `SCRIBASE_OIDC_CLIENT_ID` | oidc only | Introspection client ID |
| `SCRIBASE_OIDC_CLIENT_SECRET` | oidc only | Introspection client secret |
| `SCRIBASE_HTTP_BIND` | No | Default `127.0.0.1:8080` |
| `SCRIBASE_TRUST_PROXY_TLS` | No | Set to `1` to allow non-loopback bind |
| `SCRIBASE_MAX_ENVIRONMENTS` | No | Default `10` per new organization |
| `SCRIBASE_DATABASE_MAX_CONNECTIONS` | No | Default `10` |

### Organization single sign-on (optional)

Needed only if organizations will connect their own identity provider (see
[Single sign-on](https://docs.scribase.com/docs/concepts/sso.md)). Apply control-plane migration `0018`
first.

| Variable | Description |
|---|---|
| `SCRIBASE_PUBLIC_API_URL` | Public `https://` origin of the control API. Used for the OIDC redirect URI and the SAML entity ID and ACS URL. SSO is unavailable until it is set. |
| `SCRIBASE_CONSOLE_URL` | Console origin the browser returns to after the IdP. Default `https://console.scribase.com`. |
| `SCRIBASE_KMS_MASTER_KEY` | 64 hex characters. Seals OIDC client secrets at rest (shared with environment secrets). |
| `SCRIBASE_ORG_SETTINGS_DATABASE_URL` | Store for SSO settings, sign-in state and sessions; falls back to `SCRIBASE_CONTROL_PLANE_ADMIN_DATABASE_URL`. |
| `SCRIBASE_SSO_SESSION_HOURS` | Console session length after SSO, 1 to 168. Default `12`. |
| `SCRIBASE_DOH_URL` | DNS-over-HTTPS JSON endpoint for domain verification. Default Cloudflare's public resolver. |

### Neon integration (optional)

| Variable | Description |
|---|---|
| `SCRIBASE_NEON_API_KEY` | Neon API key for managed Neon environments |
| `SCRIBASE_NEON_PROJECT_REGION` | Default Neon region for new environments |

---

## Database roles

Scribase uses three database roles:

| Role | Privileges | Used by |
|---|---|---|
| `scribase_migrator` | `BYPASSRLS`, DDL | Schema migration runner |
| `scribase_api` | Least-privilege DML | control-api process |
| `scribase_operator` | Operator plans | Operator (`worker`) |

The `bootstrap.sql` script creates all three. The control-api process is never given the migrator or operator credential.

---

## Upgrading

1. Pull the new images or build from source.
2. Run the migration runner: `docker compose run --rm migrate`.
3. Restart services: `docker compose up -d`.

The migration runner is idempotent and checksum-guarded — it will not re-apply already-applied migrations.
