# Operations (https://docs.vitrinka.ai/operator/operations)



## Upgrading [#upgrade]

Back up `/data` first, point at the new tag, and bring it up. Migrations run on boot.

```bash
# edit .env: VITRINKA_VERSION=vX.Y.Z
docker compose up -d
```

On boot the server upgrades every workspace database that is behind the schema, `VITRINKA_MIGRATE_WORKERS` at a time (default min(8, CPUs)). `/readyz` answers 503 with the sweep's progress until it finishes, while `/healthz` stays up, so point your proxy's readiness check at `/readyz`.

* **No hard stops** — every release migrates from every prior release, so you can skip versions.
* **You never edit shipped files** — release notes may add **optional** `.env` keys; nothing you are given is meant to be modified.
* **Server before clients** — deploy the server before shipping clients that call new endpoints.

## Backups [#backup]

`/data` — the `vitrinka-data` volume — holds the SQLite databases and, unless you use S3, the blobs. Back it up on your normal schedule and **before every upgrade**.

```bash
docker compose stop vitrinka
docker compose run --rm --no-deps --entrypoint sh -v "$PWD:/backup" vitrinka \
  -c 'cd /data && tar czf /backup/vitrinka-data-$(date +%F).tgz .'
docker compose start vitrinka
```

<Callout type="warn">
  Run it **through compose**, as above. A bare `docker run -v vitrinka-data:…` mounts a different, unrelated global volume rather than the project-scoped one the app actually uses — and you would back up nothing.
</Callout>

## Your licence is not in /data [#license-location]

It lives in the bundle's `./license/` directory, mounted read-only into the container. Back up `/data` for your data and keep the bundle directory for your licence — they are two separate things to preserve.

## Air-gapped installs [#airgap]

The bundle ships an `image.tar`, a `LOAD-AIRGAP.md` and the CLI binaries. Load the image into your internal registry or straight into Docker, point `VITRINKA_IMAGE` at it, and set `AIR_GAP=1` to blank every integration that would reach the public internet — the email provider, Telegram, GitHub, the AI channel and the AI backend (`EVE_*`, `AI_BACKEND_*`), managed AI, Plane, Stripe, web push, Sentry, the OTLP trace exporter, the breach check and link-card previews — while S3, SMTP, the capture and transcription sidecars and the metrics listener keep working against your own network. A tracker, workspace AI backend or organization AI endpoint reaches that network only once deployment authority has saved it ([Outbound targets](/operator/connectors#outbound)); so does an SSO connection, and only inside the ranges `VITRINKA_SSO_PRIVATE_NETWORKS` lists ([an identity provider on your own network](/operator/sso#private)). MCP clients register at `POST /oauth/register`, because no client-ID metadata document is fetched. With Stripe blanked, plans are metered only when `VITRINKA_METERED=1` is set. The server never phones home for a version: the update check lives in the CLI, and `VITRINKA_RELEASE_FEED` below points it at your mirror.

```bash
shasum -a 256 -c SHA256SUMS    # verify the bundle first
```

## The CLI, without a registry [#cli-offline]

The bundle carries the CLI too — six static binaries, one per platform, plus `cli/latest.json`. Workstations need it to capture sets and drive the work queue, and `npm install -g` assumes a registry your estate may not have.

<Callout type="warn">
  Point `$VITRINKA_RELEASE_FEED` at your own internal mirror and `vitrinka update` keeps working offline. The bundle's `cli/README.md` carries the install instructions and the PowerShell path.
</Callout>

## Operator verbs [#verbs]

The server binary carries the maintenance commands. Every destructive one is a dry-run report until `-execute` is passed. `shred-workspace` refuses to execute without `-server-stopped`, and `reconcile-storage` without `-offline`. Stop the server for `rewrap-deks` as well, because it does not check. `seal-existing` is built to run beside a live server (`-batch`, `-pace`), and so is `platform-admin`.

| Verb                                                                                         | What it does                                                                                                    | Details                                             |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `vitrinka backup -data /data -out <file>.vtbk`                                               | Consistent copies of every database, sealed into one archive; blobs excluded.                                   | [Backups](/security/backups#archive)                |
| `vitrinka restore-backup -in <file>.vtbk -out <empty dir>`                                   | Unseals an archive into an empty directory, never over a live `DATA_DIR`.                                       | [Backups](/security/backups#restore)                |
| `vitrinka seal-existing -data /data [-execute \| -verify]`                                   | Seals legacy plaintext rows and objects in place; `-verify` is the gate before strict mode.                     | [Encryption](/security/encryption#strict)           |
| `vitrinka rewrap-deks -data /data -old-kek … -new-kek … [-execute]`                          | Rotates the deployment key by rewrapping every workspace key; data is untouched.                                | [Encryption](/security/encryption#rotate)           |
| `vitrinka shred-workspace -data /data <workspace> -server-stopped [-execute]`                | Destroys a workspace's data key and tombstones it.                                                              | [Retention and deletion](/security/retention#shred) |
| `vitrinka inventory -data /data -out <manifest>`                                             | Read-only census of every referenced blob against one listing of the store; prints a redacted summary.          | Precedes `migrate-blobs`.                           |
| `vitrinka migrate-blobs -data /data -manifest <manifest> [-execute]`                         | Copy-only move of legacy objects into their workspace prefixes; never deletes a source.                         | Idempotent; re-run after a partial failure.         |
| `vitrinka reconcile-storage -data /data -workspace <ws> -backend fs\|s3 [-execute -offline]` | Recounts a workspace's storage accounting after raw offline maintenance, taking a sealed backup first.          | Run before restarting writers.                      |
| `vitrinka audit-export-key -data /data`                                                      | Prints the public key that signs audit export batches.                                                          | [Audit export](/security/encryption#audit-export)   |
| `vitrinka audit-verify -data /data` (or `-dir <dir> -key <key>`)                             | Checks every hash link and signature of an export and exits non-zero on the first broken link.                  | [Audit export](/security/encryption#audit-export)   |
| `vitrinka platform-admin grant <email>` · `revoke <email>` · `list`                          | Opens or closes the admin console for an account that has signed in once; applies on its next request, audited. | [The first admin](/operator/install#admin)          |
| `vitrinka smtp-test -to <you@example.com>`                                                   | Sends one test mail through the `SMTP_*` relay without starting the server.                                     | [Mail](/operator/mail#check)                        |
| `vitrinka license-inspect <file>`                                                            | Parses and verifies a licence file without starting the server.                                                 | [Licensing](/security/licensing#install)            |
| `vitrinka healthcheck`                                                                       | The container health probe compose runs.                                                                        |                                                     |

Run them through compose so they see the same `/data`, keys and blob backend as the server:

```bash
docker compose stop vitrinka
docker compose run --rm --no-deps -v "$PWD/backups:/backups" vitrinka backup -data /data -out /backups/vitrinka-$(date +%F).vtbk
docker compose start vitrinka
```

<Callout type="warn">
  `backup` refuses an `-out` inside `DATA_DIR`. The sealed safety snapshots the maintenance verbs take before they rewrite data — `reconcile-storage -execute`, for one — go to `VITRINKA_BACKUP_DIR`, default `<DATA_DIR>-backups`. Inside the container that sibling path is not on the volume, so mount a directory and point the variable at it; a path inside `DATA_DIR` is refused.
</Callout>

## The admin console [#admin-console]

`/admin` is the platform console. It answers 404 to anyone who is not a platform admin; grant the flag with `platform-admin` ([the first admin](/operator/install#admin)).

* **Stats** — accounts, workspaces, organizations by plan and pending waitlist entries.
* **Workspaces** — search by slug or name. The picker sets the stored plan of a workspace's organization. While plans are not metered, each row also shows the Enterprise plan it runs on, and a note says the stored plan takes effect once metering is on — so you can stage plans before you set `VITRINKA_METERED=1`. Suspend a workspace — it then serves nothing until it is unsuspended — and review AI payments, where a managed-AI shortfall can be closed without charging, with a written reason.
* **Users** — search by email or name; platform admins carry a ★.
* **Waitlist** — with `VITRINKA_WAITLIST=1`, every new address that tries to sign in lands here. Approve it, and its next sign-in creates the account; an address with a pending invitation needs no approval.

A plan change is audited as `org.plan`, a suspension as `workspace.suspend`.

## Licensing [#licensing]

Drop your vendor-signed `license.json` into `./license/`; compose mounts it read-only at `/run/license`, which is where the server looks. How the server verifies it, what it does with a file it cannot use, what expiry pauses and how renewal works are on [Licensing](/security/licensing).
