The hosted service runs on Railway with a managed Neon (Postgres) + Upstash (Redis) stack. The sameDocumentation Index
Fetch the complete documentation index at: https://docs.letmepost.dev/llms.txt
Use this file to discover all available pages before exploring further.
apps/api/Dockerfile is the artifact you ship — there is no separate “OSS” image. Whether you use Railway, Fly, Render, ECS, or plain Kubernetes, the moving parts are the same.
Architecture: two services, one image
The API container has two entry points and you run them as two separate services in production:| Service | Start command | Public port | What it does |
|---|---|---|---|
| api | pnpm --filter @letmepost/api start:api | :3000 | HTTP, runs migrations on boot, then node dist/server.js |
| worker | pnpm --filter @letmepost/api start:worker | none | BullMQ consumer: publish, validate, refresh-token, webhook-deliver queues. node dist/queue/worker.js |
POST /v1/posts. Splitting lets you scale them independently and ensures the HTTP path stays responsive under queue pressure.
What you need
- A container host (Railway, Fly, Render, ECS, Kubernetes — anywhere Docker runs)
- Managed Postgres — Neon, RDS, Supabase, or a Postgres you operate
- Managed Redis — Upstash, ElastiCache, or a Redis you operate
- An S3 bucket configured per the media spec
- DNS control for the hostname your API will live on
- Your own platform OAuth credentials (see platform credentials)
Build the image
The repo-rootapps/api/Dockerfile builds a single image from the monorepo:
apps/api/. The Dockerfile copies pnpm-lock.yaml, every workspace package.json, and the packages/schemas/ source — none of which exist inside apps/api/.
Environment variables
Set these on both services (api and worker):| Variable | Value |
|---|---|
DATABASE_URL | Managed Postgres connection string |
REDIS_URL | Managed Redis connection string |
KEK_MASTER | openssl rand -base64 32 — same value on both services. Rotating without re-wrapping all encrypted tokens locks every connected account out. |
BETTER_AUTH_SECRET | openssl rand -base64 32 |
BETTER_AUTH_URL | https://api.your-domain.example |
PUBLIC_API_BASE_URL | https://api.your-domain.example |
TRUSTED_ORIGINS | https://dashboard.your-domain.example |
CORS_ORIGINS | https://dashboard.your-domain.example |
COOKIE_DOMAIN | .your-domain.example — leading dot, so the session cookie is readable by both the api and dashboard subdomains. |
DOCS_BASE_URL | Default https://docs.letmepost.dev. Override only if you host a separate docs site. |
AWS_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, MEDIA_PUBLIC_BASE_URL, MEDIA_ENV_PREFIX | Media bucket config — see environment. |
| Platform OAuth | Add LINKEDIN_CLIENT_ID/SECRET, TWITTER_CLIENT_ID/SECRET, etc. as the developer apps come back from review. |
NEXT_PUBLIC_API_URL=https://api.your-domain.example on its environment.
Health check
The API exposesGET /health:
/health on the api service with a 30s timeout. The worker doesn’t bind a port — disable its health check.
Migrations
The api service runs migrations on boot (node dist/db/migrate.js && node dist/server.js). The worker does not run migrations — the api owns that responsibility so two replicas don’t race.
For a schema change in your fork:
Reference: Railway
Step-by-step setup for our production deploy (api + worker on Railway, Neon for Postgres, Upstash for Redis) lives inDEPLOY.md in the repo. The two non-obvious things to know if you mirror it:
- Root Directory: blank /
/on both services. Not/apps/api. The Dockerfile needs the monorepo root as its build context. - Same
KEK_MASTERon both services. They’re separate Railway services, so it’s two env-var copies — but the value must be identical or the worker can’t decrypt tokens the api wrote.
Rollback
Container platforms handle rollback by redeploying a previous image. Migrations are forward-only — if you need to roll back across a schema change, restore the database from a point-in-time snapshot first, then redeploy the previous image.Smoke test after deploy
queued → published within a few seconds. If it stays queued, the worker isn’t consuming — see troubleshooting.
Self-host parity
There is no feature gate between the OSS image andapi.letmepost.dev. Same code, same API surface, same error envelope. The hosted tier’s only differentiation is managed OAuth apps (you don’t have to register your own with each platform) and managed infrastructure.