# Services (`services.json`) (https://lazyfrog.dev/docs/guide/services-services-json)

{/* Generated by scripts/generate-docs.ts from services/gateway/src/guide.md. Do not edit. */}

The platform runs the services defined in `/workspace/services.json`. Change them with the
`service` tool (it validates and reloads), or edit the file with `fs_write`/`fs_edit`: it is
reloaded at once, and an invalid file is reported (and not applied).

- `type`: `web` (a site or app), `api`, `worker` (background process), `cron` (scheduled job) or
  `static` (a directory served as files; with `dev`, the preview runs that dev server instead, for
  hot reload, and production serves `dir` after `build`: the shape for Vite and other SPAs).
- `dev`: the command in preview (with hot reload); `build` and `start`: production. Commands get
  `PORT` and must listen on it, on every address (`0.0.0.0` or `::`, `--port $PORT`): in production the platform picks
  the port, so a new version can start beside the live one. An app that ignores `$PORT` cannot
  be published. Every process a command starts stays in the foreground (`php-fpm -F`, `caddy
  run`, `nginx -g 'daemon off;'`; a script that starts two ends with `wait`): LazyFrog ends what a
  service started, daemons included, when it restarts or stops, but a daemon's crash and output
  aren't the service's, so the service looks healthy while that part is down.
- `build` runs in a build sandbox of its own at publish (and `deploy preflight`): don't run a
  production build (`pnpm build`, `next build`) in the preview to check your work. Beside the dev
  server it needs about as much memory again and is stopped on Free and Hobby; check with the
  type checker and tests, and with `deploy preflight` for the build itself. Run a package.json
  script beside a dev server as `node --run typecheck` rather than `pnpm typecheck`: pnpm stays
  beside the script for as long as it runs (about 50 MB), and a type check beside `next dev` in
  1 GB needs that room. On Free a type check (`tsc`) doesn't fit beside `next dev` at all, and
  there is no `deploy preflight` (it needs a paid plan): stop the dev server for the check or the
  build (`service` `stop`), run it, then `service` `restart` the dev server.
- `build` (run before a release starts: `pnpm build`, `uv sync --locked`), `test` (run against a
  copy of production at publish: `pnpm test`, `uv run --locked python -m pytest -q`, which finds
  the app from the project root; a failure warns, or stops the publish with `project` `settings`
  `block_publish_on_test_failure: true`; `DATABASE_URL`
  is that copy, and Rails merges it into its test database too, so a Rails `test` runs as
  `env -u DATABASE_URL RAILS_ENV=test bin/rails db:test:prepare test` with its own test database),
  and in the preview tests use the preview's own database: make what they need in a fixture and
  delete it after, even when a test fails (2026-11-05: a failed run left test users behind). With
  FastAPI, one `with TestClient(app)` starts the app and its pool; for several people signed in
  at once add plain `TestClient(app)` clients beside it (a second `with` closes the pool),
- `port` (web, api, static), `healthcheck_path` (must answer 200 quickly without login),
  `env` (non-secret variables), `schedule` (cron, e.g. `*/15 * * * *`), `dir` and `spa` (static).
- A `cron` service runs `start` on its schedule (`dev` instead in preview, if set), in the time
  zone named by `"timezone"` at the top of services.json (UTC if unset: set the user's, e.g.
  `"Europe/London"`). The same zone is the app's clock (`TZ`) and its database sessions' (`PGTZ`),
  so "today" is the user's today. `service run` runs it once now; `service_status` shows the next
  run. It runs only while its environment is awake: a live site sleeps after 15 quiet minutes on
  every plan unless it's always on (`project settings` `always_on: true`; Hobby keeps one live
  site awake, Pro three, Business ten; the owner can add one more for $5 / €5 a month on Billing), and a sleeping site catches up one missed run when it next wakes. For
  reminders and reports that must go out on time, turn always on for the live site and tell the
  user why.
- Ports 5432, 6432, 7070 and 2525 are taken by the platform (and 80, below the 1024 minimum).
- The first `web` service (else `static`, else `api`) is the site. Every other web, api or static
  service has its own address: `<service>--<site address>`, e.g. `https://api--p-<id>.lazyfrog.app`
  (two hyphens). Or serve it under a path of the site with a rewrite in `routing.json`.
- Services call each other at `SERVICE_<NAME>_URL` (e.g. `SERVICE_API_URL`, `http://127.0.0.1:…`),
  set for every service with a port in both environments. Never `localhost:<its declared port>`:
  the live site runs each release on ports of its own, so that works on the preview only.
- `service_status` shows what runs and whether health checks pass; `logs_tail` shows output
  (service name, or `postgres`, `migrate`, `supervisor`).
- `service stop` stops one preview service for a while and keeps it stopped until `service
  restart` starts it (or the workspace sleeps and wakes, which starts everything): the way to give
  a build or another heavy command the dev server's memory. A restart alone gives it back only
  until the next request. While the site's service is stopped, its preview address says the site
  has stopped. A live site's services aren't stopped.
- A service that keeps crashing goes to `crashed` and stops restarting: fix it, then
  `service restart` (in preview; `environment: "production"` restarts a live one, or publish the
  fix; the `service` tool says `environment` because its `env` is a service's variables, where the
  other tools say `env`). A live site's crashed service is tried again by itself every two
  minutes. A crashed service is shown to the owner and in Notifications.
- A page that loads a whole table (every row into memory, then a sort or a sum in code) works on
  a few rows and kills the service once the table grows: the sandbox's memory is the limit, and a
  restart only gives it back until the next visit. Bound it in the code: `LIMIT` and pagination
  (keyset: `WHERE id > $last ORDER BY id LIMIT 50`), aggregates in SQL (`count`, `sum`,
  `GROUP BY`), or a stream. Don't raise Node's heap (`--max-old-space-size`) above the sandbox's
  memory: the process is then killed before Node can say it ran out.
- `exec` and `db_query` run in UTC whatever `timezone` says (services and cron use it): give
  times with a zone (`AT TIME ZONE`, an aware datetime) in scripts and tests.
- **What the app declares for the platform's own steps** (top level, all optional). LazyFrog
  knows no app by name: when an app needs something at a step, you declare it here, and the
  platform runs it the same way for every app.
  - `"migrate"`: the app's own migration command, instead of the SQL files in `migrations/`
    (run on a copy of the live data at the rehearsal, then on the live data after a snapshot).
  - `"version"`: a command that prints one JSON line, `{"code": "…", "data": "…"}`: the version
    the code expects and the version the data is at (add `"compatible": false` or `true` to say
    it yourself; otherwise dotted numbers are compared). A publish, its rehearsal and a rollback
    refuse code older than the live data; `service_status` and `project get` show both. For an
    app that keeps a schema or data version of its own outside the migrations LazyFrog runs.
  - `"after_seed"`: a command run once, after the one-time copy of the preview's data into the
    live site and before it goes live, in the release being published, with `LF_PREVIEW_ORIGIN`
    and `LF_LIVE_ORIGIN` (`https://p-….lazyfrog.app` and the live address) set. A non-zero exit
    takes the copy back and publishes nothing; the last lines of its output go in the publish
    log. For data that holds the app's own address, or anything else the preview leaves in it.
  - `"vendored"`: globs from the repository root of other people's code the project keeps itself
    (`"vendored": ["lib/legacy/**", "public/vendor-x"]`; `**` any depth, `*` and `?` within a
    folder). `project_scan`, the check for keys before a publish and the commit's scan leave
    their look-alike findings out; a real provider key in them still refuses the commit.
    Installed packages and the usual third-party folders are already left out.
  - `"migrate_on_push": false`: a push (to git.lazyfrog.dev, or through the GitHub connection)
    doesn't run the preview's pending migrations; run them with `db migrate` instead. Without it
    they run on every push, on the preview's database only, after a snapshot.
