Frameworks & deploy.toml
Detection turns a repo into services and data needs from its files alone. No config file is required; deploy.toml exists only to override what detection found.
Finding the services
Rivtor walks the repo. Every folder with its own manifest (package.json, requirements.txt, pyproject.toml, go.mod, Dockerfile) is a candidate service. Monorepo tools are read to find the app folders: Turborepo, pnpm and npm workspaces, and Nx. The service is named after its folder (web, api); a single-app repo is one service.
How each service is classified
| Signal | Service type | Runs on |
|---|---|---|
| Vite, Astro, plain HTML, CRA, next export | Static site | Cloudflare's edge |
| Next.js with SSR, API routes or server actions | Web app | Container |
| Express, Fastify, NestJS, Hono | Node API | Container |
| FastAPI, Flask, Django | Python API | Container |
| Go, Rust, Ruby, Java, PHP | Generic app | Container, built with Railpack |
| Dockerfile present | Custom | Container; the Dockerfile wins |
| Celery, BullMQ, RQ worker entrypoints | Background worker | Container, no public URL |
Static sites are live in under 30 seconds. Containers are built with Railpack for zero-config Node, Python, Go and others, with your Dockerfile when one exists, and run inside a gVisor sandbox.
Finding the data needs
| Signal in code | Provisioned | Injected |
|---|---|---|
Prisma, Drizzle, TypeORM, pg, SQLAlchemy, psycopg, Django ORM | Postgres | DATABASE_URL |
redis, ioredis, Celery, BullMQ, RQ | Redis | REDIS_URL |
multer, an S3 SDK, boto3, UploadFile | Object storage (R2) | S3_ENDPOINT, S3_BUCKET, keys |
process.env.X or os.environ["X"] with no value | Nothing; asks for secret X | needs_input |
Detection also reads .env.example to learn which names your app expects. See Data services for every variable.
Wiring services together
If one service calls another, Rivtor injects the right URL. A frontend that reads NEXT_PUBLIC_API_URL or VITE_API_URL gets the API's public URL at build time. Services in one project also share a private network and reach each other by name, for example http://api:8000.
Hostnames
- The primary public service gets
<slug>.rivtor.com. - Other public services get
<slug>-<service>.rivtor.com, e.g.my-app-api.rivtor.com. - Workers get no hostname.
- Custom domains attach with
domain_add; certificates are issued automatically.
Migrations
When a migration tool is detected, its command runs against the new database before release: prisma migrate deploy, alembic upgrade head or python manage.py migrate. A failed migration stops the release; the previous one keeps serving.
Nothing is guessed silently
When detection has to assume something, the plan says so, for example “Assumed ‘api’ listens on port 8000 (uvicorn default)”, together with the deploy.toml key that changes it. Call deploy with dry_run to see the plan without deploying.
deploy.toml reference
Put deploy.toml at the repo root. Every key is optional and overrides only the field it names. Keys can be camelCase (rootDir) or snake_case and kebab-case (root_dir, root-dir). Unknown keys and wrong types are rejected with a message naming the key, for example deploy.toml: services.api.port must be an integer between 1 and 65535. A top-level project or name is tolerated but ignored; the slug comes from the deploy call.
# deploy.toml (optional). Only what you set here overrides detection.
# camelCase and snake_case keys both work: root_dir = rootDir.
ignore = ["fixtures/**", "*.mp4"]
[services.web]
root_dir = "web"
framework = "nextjs"
build_command = "pnpm build"
start_command = "pnpm start"
port = 3000
[services.api]
root_dir = "api"
kind = "api"
start_command = "uvicorn main:app --host 0.0.0.0 --port 8000"
port = 8000
health_path = "/health"
migrate_command = "alembic upgrade head"
data = ["postgres", "redis"]
[services.api.env]
LOG_LEVEL = "info"
[services.worker]
root_dir = "worker"
kind = "worker"
start_command = "celery -A tasks worker"
public = false
[services.docs]
root_dir = "docs"
kind = "static"
build_command = "npm run build"
output_dir = "dist"services.<name>
| Key | Also accepted | Type | Description |
|---|---|---|---|
rootDir | root_dir, root, path | string | Folder of the service, relative to the repo root; "." for the root. Must stay inside the repo. |
kind | — | "static" | "web" | "api" | "worker" | "custom" | What the service is. Decides where it runs and whether it gets a URL. |
framework | — | string | e.g. "nextjs", "vite", "astro", "express", "fastapi", "django". |
builder | — | "static" | "railpack" | "dockerfile" | How it is built. A Dockerfile in rootDir selects dockerfile by default. |
buildCommand | build_command, build | string | Command that builds the service. |
startCommand | start_command, start | string | Command that starts it. Not used for static sites. |
outputDir | output_dir | string | Static output folder relative to rootDir, e.g. "dist", "out", "build". |
port | — | integer 1–65535 | Port the app listens on. PORT is also injected. |
healthPath | health_path, health_check, healthcheck | string | Path that must answer before traffic moves. A leading "/" is added if missing. Default "/". |
migrateCommand | migrate_command, migrate | string | Runs against the new database before release. |
public | — | boolean | false for services with no URL. Workers are false. |
data | — | "postgres" | "redis" | "r2", or a list | Data services this service needs. When set, it replaces what detection found for that service instead of adding to it. |
env | — | table of KEY = "value" | Non-secret variables for this service. Numbers and booleans are turned into strings. Secrets go in env_set, never in the repo. |
ignore
A list of gitignore-style patterns (or a single string) that detection skips when it walks the repo, so those files never decide a service and never count towards a service's content hash. To keep files out of the upload itself, use .gitignore or .rivtorignore (see what gets uploaded).
deploy.toml: it is committed with your code. Set secrets with env_set or rivtor env set, which encrypts them and injects them only at runtime.