Services (`services.json`)
How your app's services are set up in services.json on LazyFrog: web servers, APIs, background workers, scheduled jobs and static sites.
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) orstatic(a directory served as files; withdev, the preview runs that dev server instead, for hot reload, and production servesdirafterbuild: the shape for Vite and other SPAs).dev: the command in preview (with hot reload);buildandstart: production. Commands getPORTand must listen on it, on every address (0.0.0.0or::,--port $PORT): in production the platform picks the port, so a new version can start beside the live one. An app that ignores$PORTcannot 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 withwait): 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.buildruns in a build sandbox of its own at publish (anddeploy 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 withdeploy preflightfor the build itself. Run a package.json script beside a dev server asnode --run typecheckrather thanpnpm typecheck: pnpm stays beside the script for as long as it runs (about 50 MB), and a type check besidenext devin 1 GB needs that room. On Free a type check (tsc) doesn't fit besidenext devat all, and there is nodeploy preflight(it needs a paid plan): stop the dev server for the check or the build (servicestop), run it, thenservicerestartthe 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 withprojectsettingsblock_publish_on_test_failure: true;DATABASE_URLis that copy, and Rails merges it into its test database too, so a Railstestruns asenv -u DATABASE_URL RAILS_ENV=test bin/rails db:test:prepare testwith 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, onewith TestClient(app)starts the app and its pool; for several people signed in at once add plainTestClient(app)clients beside it (a secondwithcloses the pool),port(web, api, static),healthcheck_path(must answer 200 quickly without login),env(non-secret variables),schedule(cron, e.g.*/15 * * * *),dirandspa(static).- A
cronservice runsstarton its schedule (devinstead 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 runruns it once now;service_statusshows 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 settingsalways_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
webservice (elsestatic, elseapi) 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 inrouting.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. Neverlocalhost:<its declared port>: the live site runs each release on ports of its own, so that works on the preview only. service_statusshows what runs and whether health checks pass;logs_tailshows output (service name, orpostgres,migrate,supervisor).service stopstops one preview service for a while and keeps it stopped untilservice restartstarts 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
crashedand stops restarting: fix it, thenservice restart(in preview;environment: "production"restarts a live one, or publish the fix; theservicetool saysenvironmentbecause itsenvis a service's variables, where the other tools sayenv). 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:
LIMITand 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. execanddb_queryrun in UTC whatevertimezonesays (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 inmigrations/(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": falseortrueto say it yourself; otherwise dotted numbers are compared). A publish, its rehearsal and a rollback refuse code older than the live data;service_statusandproject getshow 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, withLF_PREVIEW_ORIGINandLF_LIVE_ORIGIN(https://p-….lazyfrog.appand 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 withdb migrateinstead. Without it they run on every push, on the preview's database only, after a snapshot.
For your AI: this page as Markdown · llms.txt