Skip to content
Docs menu

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

Service classification
SignalService typeRuns on
Vite, Astro, plain HTML, CRA, next exportStatic siteCloudflare's edge
Next.js with SSR, API routes or server actionsWeb appContainer
Express, Fastify, NestJS, HonoNode APIContainer
FastAPI, Flask, DjangoPython APIContainer
Go, Rust, Ruby, Java, PHPGeneric appContainer, built with Railpack
Dockerfile presentCustomContainer; the Dockerfile wins
Celery, BullMQ, RQ worker entrypointsBackground workerContainer, 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

Data needs detection
Signal in codeProvisionedInjected
Prisma, Drizzle, TypeORM, pg, SQLAlchemy, psycopg, Django ORMPostgresDATABASE_URL
redis, ioredis, Celery, BullMQ, RQRedisREDIS_URL
multer, an S3 SDK, boto3, UploadFileObject storage (R2)S3_ENDPOINT, S3_BUCKET, keys
process.env.X or os.environ["X"] with no valueNothing; asks for secret Xneeds_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
# 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>

deploy.toml service keys
KeyAlso acceptedTypeDescription
rootDirroot_dir, root, pathstringFolder 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—stringe.g. "nextjs", "vite", "astro", "express", "fastapi", "django".
builder—"static" | "railpack" | "dockerfile"How it is built. A Dockerfile in rootDir selects dockerfile by default.
buildCommandbuild_command, buildstringCommand that builds the service.
startCommandstart_command, startstringCommand that starts it. Not used for static sites.
outputDiroutput_dirstringStatic output folder relative to rootDir, e.g. "dist", "out", "build".
port—integer 1–65535Port the app listens on. PORT is also injected.
healthPathhealth_path, health_check, healthcheckstringPath that must answer before traffic moves. A leading "/" is added if missing. Default "/".
migrateCommandmigrate_command, migratestringRuns against the new database before release.
public—booleanfalse for services with no URL. Workers are false.
data—"postgres" | "redis" | "r2", or a listData 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).

Keep secrets out of 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.