# Self-hosting

Run the whole Sandchest stack yourself — natively on Apple Silicon, or with Docker Compose and an NVIDIA GPU — then point any SDK at your own host.

Sandchest is open source end to end: the API, the queue, the inference worker and the
dashboard. This page gets a working install running, lists the environment it needs,
and shows how to point a client at it. The full operator guide, including AWS
infrastructure and cost modelling, is in
[SELFHOST.md](https://github.com/CapSoftware/Sandchest/blob/main/SELFHOST.md).

## What runs

Three processes and two stores.

| Process | What it does |
| --- | --- |
| Web (`next start`) | The API, the dashboard, and authentication |
| Queue consumer (`bun run worker`) | Claims durable jobs, retries, retention, webhook delivery |
| Inference worker (`bun run worker:inference`) | The warm Parakeet models, on Metal or CUDA |
| Postgres | Tenancy, hashed API keys, jobs, transcripts, usage |
| Object storage | Retained audio — the local filesystem, or anything S3-compatible |

All three processes must be running. Without the queue consumer the API accepts jobs
and never finishes them.

## Apple Silicon, natively

Install Bun 1.4, Rust (edition 2024), the Xcode command-line tools, and FFmpeg.

```bash
git clone https://github.com/CapSoftware/Sandchest
cd Sandchest
bun install
bun run db:migrate
bun run worker:inference
```

In a second terminal:

```bash
bun run dev
```

Open `http://localhost:3000`. With no `DATABASE_URL`, development uses a durable
embedded Postgres under `.sandchest/postgres` and stores objects in
`.sandchest/storage`; both are git-ignored. Job processing runs inside the web
process, so **do not** start a separate queue consumer in this mode — two processes
cannot open the embedded database.

The first inference start-up builds the Rust worker and downloads the pinned model
weights into the Hugging Face cache. No Python runtime is involved. If Resend is not
configured, six-digit sign-in codes are printed to the server console — in local
development only.

> [!IMPORTANT]
> When `DATABASE_URL` points at external Postgres, inline processing turns itself
> off. Start `bun run worker` in a third terminal with the same database, storage,
> inference URL and token settings as the web process.

## Docker Compose with an NVIDIA GPU

Install Docker, Docker Compose, current NVIDIA drivers and the NVIDIA Container
Toolkit, then verify GPU passthrough works. Put five independent secrets in a
git-ignored `.env`:

```bash title=".env"
BETTER_AUTH_SECRET=at-least-32-random-characters
SANDCHEST_API_KEY_PEPPER=a-different-32-or-more-random-characters
SANDCHEST_INFERENCE_TOKEN=a-third-independent-random-secret
POSTGRES_PASSWORD=a-strong-private-database-password
MINIO_ROOT_PASSWORD=a-strong-private-object-store-password
RESEND_API_KEY=your-resend-key
EMAIL_FROM=Sandchest <auth@your-verified-domain.example>
NEXT_PUBLIC_APP_URL=https://speech.your-domain.example
SANDCHEST_SELF_HOSTED=true
```

```bash
docker compose config --quiet
docker compose up --build
```

Compose brings up Postgres 17, private MinIO storage with bucket initialisation, the
CUDA Parakeet worker, the Next.js control plane and an independent queue consumer.
It refuses to start when any required signing, database, object-storage or inference
secret is missing.

The application binds to localhost by default. Set `SANDCHEST_BIND_ADDRESS` only when
an intentional public bind sits behind a properly configured HTTPS reverse proxy.

> [!WARNING]
> Docker on Apple Silicon cannot pass through an NVIDIA GPU. Use the native MLX path
> on macOS. The CUDA image targets A10/SM86 and still needs validation on your own
> NVIDIA hardware before a production rollout.

## Environment

The complete contract is
[`.env.example`](https://github.com/CapSoftware/Sandchest/blob/main/.env.example).
The variables that matter most:

| Variable | Default | Purpose |
| --- | --- | --- |
| `NEXT_PUBLIC_APP_URL` | `http://localhost:3000` | Public origin. Upload and pagination URLs are built from it. |
| `BETTER_AUTH_SECRET` | — | Session signing. At least 32 characters, required in production. |
| `SANDCHEST_API_KEY_PEPPER` | — | HMAC key for hashing API keys. At least 32 characters, independent of the above. |
| `DATABASE_URL` | embedded | Postgres connection string. Omit for embedded development Postgres. |
| `SANDCHEST_INFERENCE_URL` | `http://127.0.0.1:8765` | Where the warm model worker listens. |
| `SANDCHEST_INFERENCE_TOKEN` | — | Authenticates web and queue requests to the worker. |
| `SANDCHEST_INFERENCE_ENGINE` | `parakeet` | `parakeet`, or `whisper` when the worker was built with the optional Whisper feature. Whisper covers every accepted language code. |
| `SANDCHEST_MODEL` | `nvidia/parakeet-tdt-0.6b-v2` | English model. |
| `SANDCHEST_MULTILINGUAL_MODEL` | `nvidia/parakeet-tdt-0.6b-v3` | Model for detection and non-English audio. |
| `SANDCHEST_DEVICE` | `auto` | `auto`, `metal`, `cuda` or explicit `cpu`. `auto` requires the platform GPU. |
| `SANDCHEST_ENGLISH_REFINEMENT` | `true` | Refine uncertain detected-English transcripts on the English model. |
| `SANDCHEST_STORAGE_DRIVER` | `local` | `local` or `s3`. |
| `SANDCHEST_STORAGE_PATH` | `.sandchest/storage` | Where local objects live. |
| `SANDCHEST_S3_BUCKET` | — | Private bucket, when the driver is `s3`. |
| `SANDCHEST_S3_ENDPOINT` | — | For MinIO, R2 and other S3-compatible stores. |
| `SANDCHEST_AUDIO_RETENTION_HOURS` | `24` | How long uploaded audio is kept. |
| `SANDCHEST_MAX_UPLOAD_BYTES` | `104857600` | Upload limit in bytes — 100 MiB. |
| `SANDCHEST_RATE_LIMIT_PER_MINUTE` | `120` | Requests per minute per workspace. |
| `SANDCHEST_QUEUE_CONCURRENCY` | `4` | Parallel durable-job lanes. |
| `SANDCHEST_MAX_JOB_ATTEMPTS` | `3` | Retries before a job is failed. |
| `SANDCHEST_SELF_HOSTED` | `false` | `true` bypasses billing entitlement checks entirely. |
| `RESEND_API_KEY` | — | Email delivery for sign-in codes. Required for real logins. |
| `SANDCHEST_SQS_QUEUE_URL` | — | Optional AWS wake-up queue. Postgres stays authoritative. |

Production fails closed: missing database, cryptographic or billing configuration
stops the process rather than starting in a weakened state.

## Health

| Endpoint | Checks |
| --- | --- |
| `/api/health/live` | The web process and the database. Independent of the models, so the dashboard and login stay reachable while models warm up. |
| `/api/health` | Everything, including genuine inference-model readiness and the loaded model identifiers. |
| `:8765/health` | The inference worker itself. |
| `:8765/ready` | HTTP 503 until the model worker is usable. |

```bash
curl -fsS http://localhost:3000/api/health/live
curl -fsS http://localhost:3000/api/health
```

```json title="expected output"
{"status":"ok","database":"ok"}
```

Use `/api/health/live` as your load-balancer probe and `/api/health` for readiness
gating, so a warming model does not take the dashboard offline.

## Pointing a client at your host

Every client takes a base URL. Nothing else changes.

```ts title="@sandchest/sdk"
const client = new Sandchest({
  apiKey: process.env.SANDCHEST_API_KEY as string,
  baseUrl: "https://speech.your-domain.example",
});
```

```ts title="assemblyai"
const client = new AssemblyAI({
  apiKey: process.env.SANDCHEST_API_KEY as string,
  baseUrl: "https://speech.your-domain.example",
});
```

```python title="assemblyai (python)"
aai.settings.base_url = "https://speech.your-domain.example"
aai.settings.api_key = os.environ["SANDCHEST_API_KEY"]
```

```bash title="curl"
curl -sS "https://speech.your-domain.example/v2/transcript?limit=1" \
  -H "Authorization: $SANDCHEST_API_KEY"
```

Keys are created in your own dashboard at `/dashboard/api-keys`, exactly as on the
hosted service.

## Verify

Bring the stack up, then run the whole path end to end:

```bash
curl -fsS http://localhost:3000/api/health
```

```json title="expected output"
{
  "status": "ok",
  "database": "ok",
  "inference": "ready",
  "model": "nvidia/parakeet-tdt-0.6b-v2",
  "accepting_new_jobs": true,
  "response_ms": 12
}
```

`/api/health` returns HTTP 503 with `"status": "degraded"` until the database is
reachable and the model worker is genuinely ready.

Then sign in at `http://localhost:3000`, create an API key, and transcribe something:

```bash
curl -sS http://localhost:3000/v2/upload \
  -H "Authorization: $SANDCHEST_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @meeting.mp3
```

```json title="expected output"
{ "upload_url": "http://localhost:3000/api/v1/uploads/aud_7pKd3sTn1yBhLc8WgZaE" }
```

The repository also ships a full local verifier, which signs in with a genuine email
code, creates a workspace and key, uploads audio, transcribes through the unchanged
AssemblyAI SDK, and checks metering:

```bash
bun run verify:local
```

## Next

Back to the [docs index](/docs), or read the source at
[CapSoftware/Sandchest](https://github.com/CapSoftware/Sandchest).
