Compute Worker
Use this guide when OpenReader runs compute as a separate service. For the default embedded/local flow (pnpm dev or pnpm start without COMPUTE_WORKER_URL), configure the root .env instead and see Local Development.
What the worker does
- Runs Whisper word alignment jobs
- Runs PDF layout parsing jobs
- Runs worker-owned TTS playback planning/generation
- Generates PDF/EPUB previews and converts DOCX documents with headless LibreOffice
- Serves signed progressive MP3 playback audio directly to browsers
- Stores durable job state in NATS JetStream and NATS KV
The app server submits resource-specific operations under /v1 and listens for updates on
GET /v1/operations/:opId/events.
When to use it
- Required for Vercel-style deployments where heavy compute must run outside the app server
- Useful when you want a dedicated compute host
- Not needed for the default embedded local flow
Container image
ghcr.io/richardr1126/openreader-compute-worker:latest
Worker environment
Required worker variables:
COMPUTE_WORKER_TOKEN=...
COMPUTE_CREDENTIAL_BROKER_URL=https://reader.example.com/api/internal/compute/tts-credentials
COMPUTE_CREDENTIAL_BROKER_TOKEN=...
TTS_PLAYBACK_TOKEN_SECRET=...
NATS_URL=nats://...
S3_BUCKET=...
S3_REGION=...
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
compute-worker/.env* is only for standalone worker deployments.
- Embedded/local mode: configure the root
.envonly. - External worker mode: set
COMPUTE_WORKER_URL, optionalCOMPUTE_WORKER_PUBLIC_URL,COMPUTE_WORKER_TOKEN,COMPUTE_CREDENTIAL_BROKER_TOKEN, andTTS_PLAYBACK_TOKEN_SECRETon the app, and worker runtime values on the worker service. - Keep shared values aligned across app and worker:
COMPUTE_WORKER_TOKEN,COMPUTE_CREDENTIAL_BROKER_TOKEN,TTS_PLAYBACK_TOKEN_SECRET, andS3_*. - Do not set
AUTH_SECRET,POSTGRES_URL, orSQLITE_DB_PATHon the worker. Provider lookup and decryption remain app-owned.
Common optional variables:
NATS_CREDSorNATS_CREDS_FILES3_INTERNAL_ENDPOINT,S3_FORCE_PATH_STYLE=true,S3_PREFIX=openreaderCOMPUTE_WORKER_HOST=0.0.0.0PORT=8081for local/manual runs. Platforms like Railway usually injectPORT.LOG_FORMAT=jsonandCOMPUTE_LOG_LEVEL=infoCOMPUTE_CREDENTIAL_BROKER_TIMEOUT_MS=5000COMPUTE_PREWARM_MODELS=falseby default. Set it totrueto pre-download ONNX models during worker startup.COMPUTE_WHISPER_TIMEOUT_MS=30000COMPUTE_PDF_TIMEOUT_MS=300000COMPUTE_TTS_PLAYBACK_SEGMENT_TIMEOUT_MS=30000(defaults to the Whisper timeout)COMPUTE_PDF_JOB_ATTEMPTS=1COMPUTE_JOBS_STREAM_MAX_BYTES=268435456COMPUTE_EVENTS_STREAM_MAX_BYTES=134217728COMPUTE_JOB_STATES_MAX_BYTES=67108864COMPUTE_NATS_REPLICAS=1COMPUTE_OP_STALE_MS=1800000WHISPER_MODEL_BASE_URLPDF_LAYOUT_MODEL_BASE_URL
If you need the broader app config reference, see Environment Variables.
App server environment
Set these on the Next.js app server:
COMPUTE_WORKER_URL=https://worker.example.com
# Only needed when browsers cannot reach COMPUTE_WORKER_URL directly.
# COMPUTE_WORKER_PUBLIC_URL=https://worker-public.example.com
COMPUTE_WORKER_TOKEN=<same-token-as-worker>
COMPUTE_CREDENTIAL_BROKER_TOKEN=<same-broker-token-as-worker>
TTS_PLAYBACK_TOKEN_SECRET=<same-playback-secret-as-worker>
Notes:
- Model artifact overrides (
WHISPER_MODEL_BASE_URL,PDF_LAYOUT_MODEL_BASE_URL) belong on the worker service, not the app server. - There is no app-local compute fallback once
COMPUTE_WORKER_URLis set. If the worker is unavailable, worker-backed requests fail. COMPUTE_WORKER_URLis for app-to-worker API calls.COMPUTE_WORKER_PUBLIC_URLis the browser-facing base URL used for TTS playback audio; it defaults toCOMPUTE_WORKER_URL.COMPUTE_WORKER_TOKENis never sent to browsers. Browser audio uses short-lived signed URLs backed byTTS_PLAYBACK_TOKEN_SECRET.COMPUTE_CREDENTIAL_BROKER_TOKENis never sent to browsers or worker jobs. It authenticates only worker-to-app provider resolution.- The worker makes upstream speech-synthesis requests after resolving short-lived provider execution credentials from the app. A self-hosted provider URL must be reachable from the worker host.
Deployment notes
- App and worker must share the same object storage.
- Embedded
weed miniis not supported for external worker mode. - NATS must be reachable from the worker. The app submits operations through the worker HTTP API and does not need NATS credentials.
- Protect
COMPUTE_WORKER_TOKEN,COMPUTE_CREDENTIAL_BROKER_TOKEN, andTTS_PLAYBACK_TOKEN_SECRET. - The public
/v1/tts-playback/sessions/:sessionId/audioroute is intentionally browser-reachable, but it requires a signed playback token. Other worker routes remain protected byCOMPUTE_WORKER_TOKEN. - The worker connects to NATS lazily and disconnects after 120 seconds of full idle time. That allows platforms like Railway to sleep the service, but the first request after a cold start will be slower.
Health endpoints
GET /health/livereturns{ ok: true }.GET /health/readyreturns{ ok: true, natsConnected }and reflects the current NATS session without forcing a reconnect.
Railway + Synadia example
Deploy the worker image to Railway and set worker env vars similar to:
COMPUTE_WORKER_HOST=0.0.0.0
COMPUTE_WORKER_TOKEN=<shared-token>
COMPUTE_CREDENTIAL_BROKER_URL=https://<vercel-app-domain>/api/internal/compute/tts-credentials
COMPUTE_CREDENTIAL_BROKER_TOKEN=<shared-broker-token>
TTS_PLAYBACK_TOKEN_SECRET=<shared-playback-secret>
NATS_URL=tls://connect.ngs.global:4222
NATS_CREDS="-----BEGIN NATS USER JWT-----
...
------END USER NKEY SEED------"
S3_BUCKET=<bucket>
S3_REGION=<region>
S3_ACCESS_KEY_ID=<key>
S3_SECRET_ACCESS_KEY=<secret>
# Optional:
# S3_INTERNAL_ENDPOINT=https://private-s3-endpoint.example
# S3_FORCE_PATH_STYLE=true
# S3_PREFIX=openreader
If your platform supports mounted files, you can use NATS_CREDS_FILE instead of NATS_CREDS.
Set these on the OpenReader app server:
COMPUTE_WORKER_URL=https://<railway-worker-domain>
# Optional when browsers need a different public URL:
# COMPUTE_WORKER_PUBLIC_URL=https://<railway-worker-domain>
COMPUTE_WORKER_TOKEN=<same-token-as-worker>
COMPUTE_CREDENTIAL_BROKER_TOKEN=<same-broker-token-as-worker>
TTS_PLAYBACK_TOKEN_SECRET=<same-playback-secret-as-worker>
Verify the worker after deploy:
GET https://<railway-worker-domain>/health/liveGET https://<railway-worker-domain>/health/ready