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.
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.
git clone https://github.com/CapSoftware/Sandchest
cd Sandchest
bun install
bun run db:migrate
bun run worker:inferenceIn a second terminal:
bun run devOpen 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.
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:
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=truedocker compose config --quiet
docker compose up --buildCompose 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.
Environment#
The complete contract is
.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. |
curl -fsS http://localhost:3000/api/health/live
curl -fsS http://localhost:3000/api/health{"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.
const client = new Sandchest({
apiKey: process.env.SANDCHEST_API_KEY as string,
baseUrl: "https://speech.your-domain.example",
});const client = new AssemblyAI({
apiKey: process.env.SANDCHEST_API_KEY as string,
baseUrl: "https://speech.your-domain.example",
});aai.settings.base_url = "https://speech.your-domain.example"
aai.settings.api_key = os.environ["SANDCHEST_API_KEY"]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:
curl -fsS http://localhost:3000/api/health{
"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:
curl -sS http://localhost:3000/v2/upload \
-H "Authorization: $SANDCHEST_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @meeting.mp3{ "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:
bun run verify:localNext#
Back to the docs index, or read the source at CapSoftware/Sandchest.