Cloud sandboxes

environment.json — Automatic dev servers

Last updated: 2026-09-21

Start dev servers, watchers, and workers automatically when your environment boots or wakes. Each service in environment.json gets a named terminal tab where you can watch its output, stop it, and restart it.

What it looks like#

Create environment.json at the root of your repository and commit it:

json
{
  "services": [{ "name": "dev", "cmd": "bun install && bun dev" }]
}

Next wake, RepoGo runs the command and opens a terminal tab named dev with the live output. That's the whole setup — nothing to configure in the app.

Two habits worth forming from the first file:

  • Chain the install into cmd with &&. A fresh clone or cold boot may not have node_modules yet; install is a fast no-op when it does.
  • Start with one service. Add more as you need them.

No file yet? The web dashboard's Environment tab has a Create environment.json button that writes a starter file at the repo root and opens it in the editor. Once a file exists, the pencil in the tab header reopens it.

Which repos run it#

  • Cloud Sandbox: every repo's environment.json runs.
  • Paired Mac: only repos that belong to a GitHub account you have connected in RepoGo — cloned through RepoGo, or a folder you picked whose origin is under one of your accounts. Anything else (a repo cloned from someone else's account, a folder with no GitHub remote) is skipped: its services show as stopped and you can start them from the terminal yourself. This is deliberate — environment.json runs commands on your machine, so it only runs for code that is yours. Requires RepoGo Host 1.0.1 (16) or later; earlier versions run every repo's file.

The file#

services is the only required field. A full example:

json
{
  "version": 1,
  "services": [
    {
      "name": "api",
      "cmd": "bun install && bun run start",
      "target": "8080",
      "expose": true,
      "envFrom": ["api-secrets"]
    },
    {
      "name": "web",
      "cmd": "bun install && bun dev",
      "cwd": "apps/frontend",
      "target": "3000",
      "expose": true,
      "dependsOn": ["api"],
      "env": { "NEXT_PUBLIC_API_URL": "${ENVIRONMENT_URL_API}" }
    }
  ]
}
FieldRequiredWhat it does
nameYesTerminal tab name and how other services refer to this one. Lowercase letters, numbers, dashes: web, api, worker.
cmdYesThe command, run in a shell. Chain installs with &&; pipes, $VARS, and scripts work.
typeNo"service" (default) for long-running processes. "setup" for run-once steps that exit — installs, migrations, codegen.
cwdNoFolder to run in, relative to the repo root. Use it for monorepos instead of cd in cmd.
targetNoWhere the service serves locally: a port as a string ("3000") or a hostname ("web.localhost"). Lets other services find it. Does not make it public on its own. (port is accepted as an alias.)
exposeNotrue opens target to the internet with a public URL. Defaults to false. Requires target.
readyWhenNoReadiness check so dependents wait until this service is serving: { "port": 8080 } or { "http": "http://localhost:8080/health" }. Add "timeoutMs" to cap the wait.
dependsOnNoServices to start first. A setup is waited on until it exits; a service until it spawns, or until it is ready if it has readyWhen.
envNoExtra environment variables as name/value pairs. Can reference other services' ${ENVIRONMENT_*} variables.
envFromNoNamed secret sources to load before starting, e.g. ["api-secrets"]. Names only — the file lives on your Mac.
autoStartNoDefaults to true. false defines the service as a stopped tab you start by hand.
autoRestartNotrue restarts the service when its process exits. Off by default.
idleStopNoOn a Mac, what happens when nobody is viewing the workspace: omitted = stopped after 3 minutes, "now", or "never". See When services stop on their own.
versionNoFormat version. 1, or leave it out.

Examples#

Single app — install, run, expose:

json
{
  "services": [{ "name": "web", "cmd": "npm install && npm run dev", "target": "3000", "expose": true }]
}

Monorepo with a shared install — one setup step installs at the root; the apps depend on it and on each other:

json
{
  "services": [
    { "name": "install", "type": "setup", "cmd": "pnpm install" },
    {
      "name": "api",
      "cmd": "pnpm --filter api dev",
      "target": "8080",
      "expose": true,
      "dependsOn": ["install"],
      "readyWhen": { "port": 8080 }
    },
    {
      "name": "web",
      "cmd": "pnpm --filter web dev",
      "target": "3000",
      "expose": true,
      "dependsOn": ["install", "api"],
      "env": { "NEXT_PUBLIC_API_URL": "${ENVIRONMENT_URL_API}" }
    }
  ]
}

Public URLs#

Nothing is exposed unless you ask. To make a service public, give it a target and expose: true. On boot, RepoGo assigns it a stable public address like https://yourmac-3000-x7k2.repogo.dev — no port in the URL, generated for you so names never collide, and the same across sleeps and restarts.

expose: true is re-applied on every wake, so you never have to turn it back on. You can also open or close targets from the environment's screen in the app at any time.

Portless: friendly hostnames#

If your dev setup serves on hostnames instead of ports — web.localhost, api.localhost — set target to the hostname and RepoGo proxies straight to it. Two apps that both want 3000 can't clash when each owns a name, and expose, dependsOn, readyWhen, and the ${ENVIRONMENT_*} variables all work the same.

json
{
  "services": [
    { "name": "web", "cmd": "bun install && bun dev", "target": "web.localhost", "expose": true },
    { "name": "api", "cmd": "bun install && bun start", "target": "api.localhost", "expose": true }
  ]
}

A bare label is shorthand: "target": "web" means web.localhost. Anything with a dot, or the literal localhost, is used as written.

RepoGo forwards requests to http://<hostname> inside the environment with the right Host header; your project or a small local proxy has to actually answer on that name. If a hostname target reports "local server unreachable," nothing inside the box is serving it yet. A port target always works with no extra setup.

Connecting services to each other#

Don't hardcode another service's address. Every service gets variables describing the other services, usable in cmd or env:

VariableExampleAvailable when
ENVIRONMENT_TARGET_<NAME>8080the other service has a target
ENVIRONMENT_URL_<NAME>https://yourmac-8080-x7k2.repogo.devthe other service is exposed
ENVIRONMENT_DOMAIN_<NAME>yourmac-8080-x7k2.repogo.devthe other service is exposed

<NAME> is the other service's name uppercased, dashes to underscores: my-api → ENVIRONMENT_URL_MY_API.

  • URL — full https://…. For anything a browser hits: NEXT_PUBLIC_API_URL, OAuth callbacks, webhooks.
  • DOMAIN — host only. For CORS allowlists and cookie domains.
  • TARGET — the local port or hostname. For server-to-server calls inside the environment: http://localhost:$ENVIRONMENT_TARGET_API (port) or http://$ENVIRONMENT_TARGET_API (hostname).

A private service (target, no expose) provides only TARGET. These values are read at start, so if you change a URL while things are running, restart the services that use it.

Secrets stay on your Mac#

The repo says what a service needs; the app says where it lives.

  1. In environment.json, list named sources: "envFrom": ["api-secrets"]. Names only, nothing sensitive.
  2. In the app's Env Sources, point each name at a file on your Mac, once.

Save or refresh the source in Env Sources to pull a protected snapshot to your iPhone. When a service starts, approve sharing that snapshot; RepoGo loads the variables into the service in memory without writing them to the environment's disk or a snapshot. A teammate points the same name at their own file, so one environment.json works for everyone. Full flow: Env Sources.

When services stop on their own#

On a Mac, every project shares one machine, so RepoGo stops managed services shortly after you stop using a workspace:

  1. While any tab or device has the workspace open, everything stays up.
  2. When the last one closes, a 3-minute countdown appears on the workspace tab, next to a Kill app button.
  3. Come back before it runs out and nothing stops.
  4. When it expires, RepoGo stops the services it started. Terminals you opened by hand are left alone.

idleStop changes this per service:

idleStopWhen you stop viewing the workspace
(omitted)Stopped after the 3-minute countdown. Right for almost everything.
"now"Stopped immediately. For a heavy dev server or a port you want back at once.
"never"Never stopped by the countdown. For a local database or a tunnel.

Kill app always stops everything, including "never" services. If every service is "never", no countdown is shown.

Cloud Sandboxes ignore idleStop: a sandbox is scoped to one project and already stops everything when it sleeps, so its services stay up the whole time it is awake.

Multiple repositories#

An environment can hold several repos, each with its own environment.json. On wake, RepoGo starts the services from every repo. A repo without the file starts nothing. Service names are scoped per repo, so two projects can both have web. dependsOn only orders services within the same repo.

Next steps#