Writing platform.yml

platform.yml is a file at the root of your repo that tells Tandem what services live inside it and how to build each one. When you connect a repo (or push a new commit), Tandem reads this file to plan deployments.

A minimal example

project: my-app

services:
  web:
    path: web
    type: static
    build: npm run build
    output: dist

  api:
    path: api
    type: node
    build: npm ci
    start: npm start
    healthcheck: /health

That’s a complete file — a project name and one or more services.

Top-level fields

Field Required Description
project yes A name for the project. Lowercase identifier, used as part of the auto-assigned hostname.
services yes A map of service name → service config. At least one service is required.
ignored no A list of directory names to record as “intentionally not deployed”. Purely informational — they don’t gate the build.

The file is strict: unknown top-level keys cause a parse error.

Service fields

Each entry under services is a map with these fields:

Field Required Default Notes
path yes Path to the service relative to the repo root. Use . for repo-root services.
type yes One of static, node, or dockerfile. See below.
install no auto Override the default install step. See “Monorepos” below.
build no Build command, run after install. Required for static; optional for node.
start no Command to launch the running container. Required for node.
output no Directory (relative to path) containing the built static files. Required for static.
healthcheck no / HTTP path the deploy worker polls until it returns 2xx before flipping the route.
healthcheckTimeout no platform default Seconds to wait for the healthcheck to pass. Between 10 and 1800.
spa no true for static When true, the nginx config falls back to index.html for unknown paths so client-side routing survives a refresh. Ignored for non-static services.

Service-config keys are also strict — typos will fail parsing rather than be silently ignored.

Service types

static

Used for sites with a build step that produces a folder of HTML/CSS/JS assets. The build runs in node:24-alpine and the output folder is served by nginx on port 80.

You must set build and output. The SPA fallback is on by default (spa: true); set spa: false if you’re shipping a multi-page static site that should let nginx 404 unknown paths.

services:
  web:
    path: web
    type: static
    build: npm run build
    output: dist

node

Used for long-running Node processes (Express, Fastify, Next.js in server mode, anything that listens on a port). The container runs node:24-alpine and Tandem injects PORT=3000 — your start command must listen on process.env.PORT.

You must set start. build is optional but useful for compile steps (TypeScript, Next build, Prisma generate). NODE_ENV is intentionally not set during build, so npm install keeps devDependencies — set NODE_ENV=production via a runtime env var if you need it at runtime.

services:
  api:
    path: api
    type: node
    build: npm run build
    start: node dist/server.js
    healthcheck: /health

dockerfile

For when you want full control. Tandem just runs docker build in your service path, so you provide the Dockerfile. build, start, output, and install are ignored — your Dockerfile owns all of that.

services:
  worker:
    path: worker
    type: dockerfile
    healthcheck: /health

Your container should listen on the port you EXPOSE and respond to the configured healthcheck path.

Monorepos and install:

By default, Tandem runs npm ci (or npm install if there’s no lockfile) inside the service’s path directory. That’s wrong for npm/pnpm/yarn workspaces, where dependencies must be resolved from the repo root.

Override with install:. The command runs with the full repo mounted at /workspace and the working directory set to /workspace/<path>, so you can cd back to the root:

services:
  api:
    path: packages/api
    type: node
    install: cd /workspace && npm install
    build: cd /workspace && npm run build --workspace=api
    start: node dist/server.js

Environment variables

platform.yml doesn’t carry env vars — set them in the portal (Service → Env). Each variable has a scope:

  • build — available as a --build-arg during docker build. Use this for NEXT_PUBLIC_* and other client-bundled values.
  • runtime — passed via --env-file when the container starts.
  • both — available in both phases.

The platform always sets PORT=3000 at runtime for node services.

Recipes

Next.js (server mode)

services:
  web:
    path: web
    type: node
    build: npm ci && npm run build
    start: npm start
    healthcheck: /

Set NEXT_PUBLIC_* env vars with scope build so they’re inlined at build time.

Vite SPA

services:
  web:
    path: web
    type: static
    build: npm ci && npm run build
    output: dist

Vite’s default output dir is dist. Set VITE_* env vars with scope build.

Plain Node / Express

services:
  api:
    path: api
    type: node
    build: npm ci
    start: node server.js
    healthcheck: /health

Make sure server.js calls app.listen(process.env.PORT).

Bring-your-own Dockerfile

services:
  go-worker:
    path: services/worker
    type: dockerfile
    healthcheck: /health

The Dockerfile lives at services/worker/Dockerfile. Inside it, EXPOSE your port and make sure it serves the healthcheck path.

Common errors

  • platform_yml_not_found — the file is missing from the branch Tandem fetched. Check it’s at the repo root and committed to the default branch.
  • platform.yml does not define service <name> — the service exists in Tandem’s database but isn’t in your services: block. Either add it or remove the service from the portal.
  • service <name> type mismatch — the type in platform.yml doesn’t match what was configured in the portal. Update one to match the other.
  • static service ... requires a build command / requires an output directory — add build: and output: to your static service.
  • node service ... requires a start command — add start:.
  • Strict-mode parse errors — you used a key Tandem doesn’t recognize, or got the casing wrong (e.g. healthCheck instead of healthcheck). Check the field reference above.