Skip to content

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.

Open .md

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.

ProcessWhat 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
PostgresTenancy, hashed API keys, jobs, transcripts, usage
Object storageRetained 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.

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

In a second terminal:

shell
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.

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:

.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
shell
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.

Environment#

The complete contract is .env.example. The variables that matter most:

VariableDefaultPurpose
NEXT_PUBLIC_APP_URLhttp://localhost:3000Public origin. Upload and pagination URLs are built from it.
BETTER_AUTH_SECRETSession signing. At least 32 characters, required in production.
SANDCHEST_API_KEY_PEPPERHMAC key for hashing API keys. At least 32 characters, independent of the above.
DATABASE_URLembeddedPostgres connection string. Omit for embedded development Postgres.
SANDCHEST_INFERENCE_URLhttp://127.0.0.1:8765Where the warm model worker listens.
SANDCHEST_INFERENCE_TOKENAuthenticates web and queue requests to the worker.
SANDCHEST_INFERENCE_ENGINEparakeetparakeet, or whisper when the worker was built with the optional Whisper feature. Whisper covers every accepted language code.
SANDCHEST_MODELnvidia/parakeet-tdt-0.6b-v2English model.
SANDCHEST_MULTILINGUAL_MODELnvidia/parakeet-tdt-0.6b-v3Model for detection and non-English audio.
SANDCHEST_DEVICEautoauto, metal, cuda or explicit cpu. auto requires the platform GPU.
SANDCHEST_ENGLISH_REFINEMENTtrueRefine uncertain detected-English transcripts on the English model.
SANDCHEST_STORAGE_DRIVERlocallocal or s3.
SANDCHEST_STORAGE_PATH.sandchest/storageWhere local objects live.
SANDCHEST_S3_BUCKETPrivate bucket, when the driver is s3.
SANDCHEST_S3_ENDPOINTFor MinIO, R2 and other S3-compatible stores.
SANDCHEST_AUDIO_RETENTION_HOURS24How long uploaded audio is kept.
SANDCHEST_MAX_UPLOAD_BYTES104857600Upload limit in bytes — 100 MiB.
SANDCHEST_RATE_LIMIT_PER_MINUTE120Requests per minute per workspace.
SANDCHEST_QUEUE_CONCURRENCY4Parallel durable-job lanes.
SANDCHEST_MAX_JOB_ATTEMPTS3Retries before a job is failed.
SANDCHEST_SELF_HOSTEDfalsetrue bypasses billing entitlement checks entirely.
RESEND_API_KEYEmail delivery for sign-in codes. Required for real logins.
SANDCHEST_SQS_QUEUE_URLOptional 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#

EndpointChecks
/api/health/liveThe web process and the database. Independent of the models, so the dashboard and login stay reachable while models warm up.
/api/healthEverything, including genuine inference-model readiness and the loaded model identifiers.
:8765/healthThe inference worker itself.
:8765/readyHTTP 503 until the model worker is usable.
shell
curl -fsS http://localhost:3000/api/health/live
curl -fsS http://localhost:3000/api/health
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.

@sandchest/sdk
const client = new Sandchest({
  apiKey: process.env.SANDCHEST_API_KEY as string,
  baseUrl: "https://speech.your-domain.example",
});
assemblyai
const client = new AssemblyAI({
  apiKey: process.env.SANDCHEST_API_KEY as string,
  baseUrl: "https://speech.your-domain.example",
});
assemblyai (python)
aai.settings.base_url = "https://speech.your-domain.example"
aai.settings.api_key = os.environ["SANDCHEST_API_KEY"]
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:

shell
curl -fsS http://localhost:3000/api/health
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:

shell
curl -sS http://localhost:3000/v2/upload \
  -H "Authorization: $SANDCHEST_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @meeting.mp3
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:

shell
bun run verify:local

Next#

Back to the docs index, or read the source at CapSoftware/Sandchest.