# Configuration

What an operator can set, and where Sbarbase keeps its state. Paths are relative to the checkout. Sbarbase has no single configuration file: settings live in the files and flags below, and everything else is decided by code and pinned lock files.

## Host requirements

| Setting | Where | Notes |
|---|---|---|
| Docker endpoint | `DOCKER_HOST`, or `--docker-host` on `deploy/server-acceptance.sh` | Must reach a native Linux daemon. A system service does not inherit your shell, so a host whose Docker context points at a desktop socket must forward it in the unit |
| Bun | on `PATH`; `--bun-dir` for the service unit | Used for the console build and the loopback server |
| Python | `/usr/bin/python3`, 3.12 or newer, with `cryptography` | `--python` on `deploy/server-acceptance.sh` overrides it |

## Pinned images

`lab/distro-image.lock.json`, `lab/images.lock.json`, `lab/storage-image.lock.json`, `lab/realtime-image.lock.json`, `lab/functions-image.lock.json` and `lab/studio-image.lock.json` pin every upstream image by tag and digest. Change them only through `lab/pin_update.py`; see [upgrades](https://base.sbarah.com/en/docs/guides/upgrades/).

## Installer and service

`/usr/bin/python3 lab/install_server.py <command>`:

| Command or flag | Meaning |
|---|---|
| `check` | Read-only preflight; non-zero exit on any blocker |
| `plan` | Print the exact install steps |
| `install --bootstrap-file PATH` | Install, using a 0600 operator file (below) |
| `images` | Check the Docker profile, then pull and verify every pinned image |
| `smoke` | Check the console and management endpoints answer |
| `supervise` | Render the systemd unit for this checkout and verify it; `--apply` installs it (root only), `--defer-start` installs and enables it without starting it |
| `wait-console` | Wait until the console answers; `--timeout` sets the seconds, also for `supervise --apply` |
| `--service-user`, `--home`, `--bun-dir` | Account, home directory and Bun directory written into the unit. Default account `sbarbase` |

`deploy/server-acceptance.sh` accepts `--rehearse`, `--skip-install`, `--install-unit`, `--first-project`, `--bootstrap-file`, `--service-user`, `--home`, `--bun-dir`, `--docker-host`, `--console-timeout` and `--python`. The shipped unit is `deploy/sbarbase.service`; do not hand-edit an installed copy, render it with `supervise`.

## TLS proxy

`bun deploy/console-tls-proxy.ts` terminates HTTPS in front of the loopback server.

| Flag | Default | Meaning |
|---|---|---|
| `--cert`, `--key` | required | Certificate and key files |
| `--public-host` | required | Public host name (optional port); used for redirects instead of the client's `Host` |
| `--https-port` | `8443` | HTTPS listener |
| `--http-port` | `8080` | Plain HTTP listener that redirects with `308` |
| `--upstream` | the running loopback server | Where to forward |
| `--max-body` | the upload limit plus 1 MiB (51 MiB) | Larger bodies get `413`. Bodies up to 1 MiB are read whole; larger ones (file uploads) are passed on as they arrive |
| `--studio-host` | `SBARBASE_STUDIO_HOST` | The Studio address pattern, such as `studio-{id}.example.com`, whose hosts the proxy also serves. Any host that is neither the public host, loopback nor a matching Studio address gets `421` |

`/usr/bin/python3 lab/studio_address.py plan|apply|verify DOMAIN` sets up Studio's public address once: `plan` lists the wildcard DNS record, the certificate and the setting to provide, `apply` (as root) writes `SBARBASE_STUDIO_HOST` as a systemd drop-in for the console (`--service`, default `sbarbase`) and the proxy (`--proxy-service`, default `sbarbase-proxy`) and restarts both, and `verify` says which step is missing ([Studio](https://base.sbarah.com/en/docs/guides/studio/#open-it-from-anywhere)).

## Operator bootstrap file

`/usr/bin/python3 lab/operator_file.py PATH` prompts (no echo) and writes a 0600 JSON file with exactly `email`, `password` and `organization`. `--stdin` reads the same object from stdin (at most 8192 bytes) for automation. Pass it to `install --bootstrap-file`, or run `lab/bootstrap.py` interactively instead. See [operator setup](https://base.sbarah.com/en/docs/guides/operator-setup/).

## Per-environment mail

Application mail (confirmation, recovery) for one environment is switched on by one file, `.secrets/upstream/<runtime>-mail.json`, mode 0600. Write, show or remove it with `lab/mail_config.py write|show|remove PATH`; the password is never printed. Keys: `host`, `port`, `user`, `pass`, `admin_email`, `sender_name`, `reply_to`, `max_frequency`, `otp_exp`, `otp_length`, `secure_email_change`, `autoconfirm`. A change is applied by reconciling that environment's Auth container. Full schema: [environment email](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/ENVIRONMENT-EMAIL.md).

## Operator notifications

If `.lab/upstream/notifications.json` exists, the worker delivers operator events by email, by signed webhook, by Telegram, or any mix:

```json
{
  "schema": 1,
  "email": {"enabled": true, "host": "smtp.example.invalid", "port": 587,
            "from": "sbarbase@example.invalid", "to": "operator@example.invalid", "tls": "starttls"},
  "webhook": {"enabled": true, "url": "https://hooks.example.invalid/sbarbase",
              "secretFile": ".secrets/upstream/notifier.json"},
  "telegram": {"enabled": true, "chatId": "-1001234567890",
               "tokenFile": ".secrets/upstream/telegram.json"}
}
```

`tls` is `none`, `starttls` or `tls`. The webhook signing secret lives in `.secrets/upstream/notifier.json` as `{"schema": 1, "webhookSecret": "<64 hex characters>"}`. For Telegram, create a bot with @BotFather, add it to your chat, and put its token in a private file (mode 0600) as `{"schema": 1, "botToken": "<token>"}`. `chatId` is the chat's number, or `@channelname` for a public channel. The token is read from that file only and never appears in a message. Full design: [operator notifications](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/OPERATOR-NOTIFICATIONS.md).

**Telegram from the console.** An owner of the installation can connect Telegram without editing files: **Notifications** in the console's Installation menu. Paste the bot token from @BotFather; Sbarbase checks it with Telegram, keeps it in `.secrets/upstream/telegram.json` (mode 0600) and never shows it again, only its last four characters. Then open the link the page gives and press **Start** in Telegram: only the chat that opens that one-time link (valid 30 minutes) is connected, and a confirmation arrives there. The page also sends a test message and disconnects, which forgets the token. Other channels in `notifications.json` are kept as they are, and a change applies to the running installation within a second, without a restart. Installation admins see the state; only owners change it, and each change is in the audit log. If a token was ever exposed, send `/revoke` to @BotFather and paste the new one.

**Disk space and stopped services.** The supervisor looks once a minute. A filesystem it writes to (the checkout, and Docker's data directory) with less than 10 % or 5 GiB free sends a warning, under 5 % or 2 GiB a critical notice. The database, Storage, the management sign-in, or an environment's Auth, REST or Realtime container that stays stopped for five minutes sends a critical notice. Edge Functions are not watched, because they stay stopped on purpose while an environment is paused.

**Checking from outside.** A server that is down cannot report itself. `deploy/uptime-worker/` is a Cloudflare Worker that asks the public `/health` route every five minutes: two failures in a row send one Telegram message, and the first good answer after that sends another. Copy `wrangler.example.toml` to `wrangler.toml` there (git ignores it), set `HEALTH_URL` and the id of a KV namespace for its state (`bunx wrangler kv namespace create sbarbase-uptime`), deploy it with `bunx wrangler deploy -c deploy/uptime-worker/wrangler.toml`, and give it the bot token and the chat id as secrets with `bunx wrangler secret put TELEGRAM_TOKEN` and `bunx wrangler secret put CHAT_ID` (same `-c`); neither is ever in a file. The chat id is the `chatId` the console wrote to `.lab/upstream/notifications.json`.

Among the events: **an environment kept needing more than its share.** The gateway guarantees each environment 8 requests in flight and lets a busy one borrow up to 24 of the 64 (8 for each of the eight environments an installation may hold), leaving free the unused share of every project active in the last minute (and at least 8). Borrowing alone is never reported. After 15 minutes in a row of being refused at its limit, or of crowding out a neighbour, you get one notice for that environment, with what to do: raise its share (the environment page in the console, **Gateway share**; installation operators only), move it to its own database engine, or grow the server. A share can be raised only while the shares of all ready environments fit in the gateway's 64; lower another first if they do not. Design: [fair share admission](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/FAIR-SHARE-ADMISSION.md).

## Off-host backup copies

If `.lab/upstream/backup-offsite.json` exists, each daily backup run is encrypted and copied to one S3-compatible bucket:

```json
{
  "schema": 1,
  "s3": {"endpoint": "https://s3.eu-central-1.amazonaws.com", "region": "eu-central-1",
         "bucket": "example-backups", "prefix": "sbarbase/",
         "credentialsFile": ".secrets/upstream/offsite-s3.json"},
  "keyFile": ".secrets/upstream/offsite-key.json"
}
```

`endpoint` must be `https` (plain `http` only on loopback). `prefix` may hold letters, digits, `.`, `_`, `-` and `/`. `credentialsFile` is a 0600 file `{"schema": 1, "accessKeyId": "...", "secretAccessKey": "..."}`. `keyFile` is a 0600 file `{"schema": 1, "key": "<64 hex characters>"}`, created only on request with `python3 lab/backup.py offsite-key PATH`. Both files are refused when they are symlinks or readable by group or others. Retention follows `SBARBASE_BACKUP_KEEP`. Setup and limits: [backup and restore](https://base.sbarah.com/en/docs/guides/backup-and-restore/).

## Update channel

The update settings, requests and the upgrade record live in `.lab/upgrades/`, private to the service account: the directory is 0700, and the JSON files are 0600 and each replaced atomically. The console writes only `settings.json` and `request.json`; the supervisor and `lab/upgrade.py` write the rest ([upgrades](https://base.sbarah.com/en/docs/guides/upgrades/)).

| Path | Holds |
|---|---|
| `.lab/upgrades/settings.json` | The operator's settings: `check` (default on), `automatic` (default off) and the maintenance `window` in server local time (default 3:00 AM to 5:00 AM, stored as `"03:00"` and `"05:00"`). A missing or damaged file means the defaults |
| `.lab/upgrades/request.json` | The one request under way (`apply`, `rollback` or `check`, with `acknowledged` for an attended release), created only when none exists; the supervisor judges it again before acting |
| `.lab/upgrades/last-request.json` | The last finished request, for the console's progress view |
| `.lab/upgrades/current.json` | The supervisor's word to the console: the running version, the `rollback` verdict, the `apply` verdict for the release on offer (can it be installed now, the reason, whether it needs the acknowledgement), `pending` while an upgrade or rollback waits, and the `timezone` the window is read in |
| `.lab/upgrades/available.json` | The last check's result: the running version, the release on offer with its class, notes and refusals, and the newer releases passed over with the reason |
| `.lab/upgrades/available.previous.json` | A check result made on another version, moved aside so the console never offers the release just installed |
| `.lab/upgrades/check.json` | When the last check ran, its error and the count of failures in a row (for the backoff) |
| `.lab/upgrades/outcome.json` | How the last update child the supervisor ran ended (passed its point of no return, changed anything, refusals, error), for the request's detail and the automatic tries |
| `.lab/upgrades/ledger.json` | Per version: announced, automatic tries, whether a try was spent past its point of no return, and rolled back |
| `.lab/upgrades/state.json` | The last upgrade: from, to, phase (`applied`, `confirmed`, `rolling_back`, `rolled_back`, `rollback_failed`, `failed`), who started it, its snapshot, the guard's attempt and failed move counts, why it went back, a failed rollback of a confirmed version, whether it is stuck, and notices the next start sends |
| `.lab/upgrades/snapshots/` | Control state snapshots (catalog and key store) with a manifest; the newest three are kept, plus the one the last upgrade names |
| `.lab/upgrades/guard.py` | The start guard of the version the last upgrade left, which every start runs first (see [the start guard](https://base.sbarah.com/en/docs/guides/upgrades/#the-start-guard)) |
| `.lab/upgrades/hold` | Present while a new version waits for its health checks; the gateway holds application traffic only while `state.json` also says a start is pending |
| `.lab/upgrades/probe-token` | A random token for this start's health checks through the gateway, 0600; it exists only while the hold does |
| `.lab/upgrades/evidence-<time>/` | Evidence written on this server, copied aside before the checkout moved (by an upgrade or a way back) |
| `.lab/upgrades/aside-<time>/` | Local changes to tracked files outside `docs/evidence/`, and untracked files the previous version would overwrite, copied here by a way back before it forced the checkout; one folder per way back. Nothing prunes them: remove one yourself once you no longer need it |
| `.lab/upgrades/upgrade.lock`, `channel.lock`, `check.log`, `apply.log`, `rollback.log` | The lock one upgrade or rollback holds, the lock of one release check or fetch, and the output of the last child of each kind |
| `.lab/upstream/worker-drain` | Present while the supervisor drains before an update: the worker claims no new job |
| `.lab/upstream/upgrade-intent.json` | Which pinned images the next start may replace, written by an upgrade or a way back |

`SBARBASE_RELEASE_SOURCE` names the Git repository the release check reads (a URL or a path); by default the canonical repository over HTTPS. Signatures are checked against `deploy/release-signers` of the running checkout whatever the source. `TZ` sets the zone the maintenance window is read in; `compose.yaml` passes both into the container (see [operator settings](#operator-settings)).

## State directories

Both are ignored by Git. Never print `.secrets/`, and never delete either to get past a refusal.

| Path | Holds |
|---|---|
| `.lab/upstream/control.sqlite` | The catalog: clients, projects, environments, memberships, jobs, routing |
| `.lab/upstream/server.json` | URL and PID of the running loopback server |
| `.lab/upstream/*.json` | Operation journals, recovery descriptors and probe outputs |
| `.lab/ui/` | The built console |
| `.lab/backups/` | Daily backups per environment, and the installation manifest of each run under `installation/` |
| `.lab/backups/upgrade-moved.json` | The backup runs of upgrades that moved the checkout. The last 3 of them are kept out of pruning; an upgrade try that stopped before the move keeps ordinary backups |
| `.secrets/upstream/runtime.json` | Generated credentials of the owned runtime |
| `.secrets/upstream/managed-keys.sqlite` | API key metadata: secret keys as hashes only, publishable keys also kept so the console can show them again |
| `.secrets/upstream/access-tokens.sqlite` | Access tokens (`sbp_...`) as SHA-256 digests, with their scope and expiry |
| `.secrets/upstream/studio-session.key` | The key that signs Studio entry tickets and session cookies |
| `.secrets/upstream/telegram.json` | The Telegram bot token, when connected from the console |
| `.secrets/upstream/bootstrap.json` | Operator setup journal (no password) |
| `.secrets/offsite.json` | Off-site copy settings, the storage key and the passphrase ([backup and restore](https://base.sbarah.com/en/docs/guides/backup-and-restore/#copies-off-the-server)) |
| `.secrets/upstream/<runtime>-auth.json` | An environment's sign-in settings, including provider secrets, written by the console ([sign-in](https://base.sbarah.com/en/docs/guides/sign-in/)) |

## Operator settings

Set these in `compose.yaml` (Docker) or as `Environment=` lines of the systemd service.

| Variable | Default | Meaning |
|---|---|---|
| `SBARBASE_STUDIO_HOST` | `{id}.studio.localhost` | Each environment's Studio address, with `{id}` for its 24 hex characters, such as `studio-{id}.example.com`. The default works through an SSH tunnel; a public pattern needs a wildcard DNS record and a certificate that covers it, and the TLS proxy reads the same variable (`lab/studio_address.py` writes it for both; [Studio](https://base.sbarah.com/en/docs/guides/studio/#open-it-from-anywhere)). A change applies at the next start |
| `SBARBASE_PUBLIC_URL` | `http://localhost` | The address people and OAuth providers reach this server at, such as `https://api.example.com`. Auth builds email links and OAuth callbacks from it. A change applies at the next start |
| `SBARBASE_CONSOLE_PORT` | chosen at start; `8790` with Docker | The loopback port of the console and API, for a TLS proxy |
| `SBARBASE_BACKUP_HOUR` | `3` | Hour (UTC) of the daily backup; `off` stops it |
| `SBARBASE_MANAGEMENT_SESSION_SECONDS` | `2592000` | Seconds one password sign-in may keep a management session alive, 3600 to 2592000 (thirty days). Empty, invalid or out of range means the default, not the nearest limit. A change needs a service restart: lowering then cuts existing sessions at their next request, raising applies to grants issued afterwards. The console keeps the session in the browser, readable by scripts on its own origin, until sign out ([details and where to set it](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/MANAGEMENT-MFA.md)) |
| `SBARBASE_BACKUP_KEEP` | `7` | Backups kept per environment. Backups taken for an upgrade are not counted: those of the last 3 upgrades that moved the checkout are always kept |
| `TZ` | UTC in the container | The zone the maintenance window of automatic updates is read in, for example `Asia/Dubai`. Outside Docker the server's own zone applies |
| `SBARBASE_RELEASE_SOURCE` | the canonical repository | Where the update check reads signed releases: a mirror's Git URL or path |
| `SBARBASE_DATABASE_PORT` | `6543` | Port of the direct database access listener ([database access](https://base.sbarah.com/en/docs/guides/database-access/)); `off` turns it off |
| `SBARBASE_DATABASE_BIND` | `127.0.0.1` | Address that listener binds. Keep loopback and use an SSH tunnel; another address sends database traffic unencrypted |
| `SBARBASE_UPLOAD_LIMIT_MB` | `50` | The largest file an application may upload to Storage, in MiB (1 to 5120), as Supabase's global file size limit. Larger uploads get `413`. A change applies at the next start, which recreates the shared Storage container; a bucket's own limit can be lower ([evidence](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/docker-upload-checks.json)) |

With Docker, `deploy/compose.sh` also reads these. It refuses when the socket or Docker's data directory is missing, or when `SBARBASE_ROOT` is not this checkout:

| Variable | Default | Meaning |
|---|---|---|
| `SBARBASE_ROOT` | the checkout | The checkout, mounted at the same path inside the container; it must be the checkout `deploy/compose.sh` runs from |
| `SBARBASE_DOCKER_SOCKET` | `/var/run/docker.sock` | The local Docker socket the container controls |
| `SBARBASE_DOCKER_DATA_ROOT` | `/var/lib/docker` | Docker's data directory, mounted read only so block IO limits find its disk |
| `SBARBASE_COMPOSE_PROJECT` | `COMPOSE_PROJECT_NAME`, else `sbarbase` | The Compose project name |

## Internal environment variables

Other variables named `SBARBASE_*` (for example `SBARBASE_WORKER_FD`, `SBARBASE_EFFECT_TOKEN`, and `SBARBASE_DOCKER_PROFILE` and `SBARBASE_CONTAINER`, which `compose.yaml` sets) pass lock descriptors and tokens between Sbarbase's own processes. They are not operator settings; do not set them by hand.

## Not configurable yet

Resource tiers and admission thresholds are fixed in code ([lab/resource_policy.py](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/lab/resource_policy.py), [lab/pressure_admission.py](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/lab/pressure_admission.py)). Gateway concurrency limits and deadlines are constructor defaults in [src/gateway/concurrency.ts](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/src/gateway/concurrency.ts).
