midstream.json
The file that tells Midstream how to install, start, and test your app.
To open an instance, Midstream has to run your app the way you would run it
yourself. You tell it how in midstream.json, a file in the root of your
repository. Nothing is guessed. If the file is missing, the deploy stops before
anything else happens.
The file
{
"install": "npm ci",
"start": "npm start",
"test": "npx playwright test {{file}} -g \"{{grep}}\""
}| Key | Required | What it does |
|---|---|---|
install | Yes | Runs once, before the app starts. Install dependencies, build, migrate, seed. |
start | Yes | Starts your app and keeps running. |
test | No | Runs one Playwright test. Defaults to npx playwright test {{file}} -g {{grep}}. |
They run in that order: install, then start, then — as soon as your app answers on its port — the test that gets the app to the scene.
install and start must both be present and must both be non-empty strings.
Anything else in the file is ignored.
Every command runs from the repository root
Midstream clones your repository, and runs all three commands with the repository root as the working directory. There is no way to point a command at a subdirectory, so your app has to be installable and startable from the root.
That is worth checking before anything else if you are in a monorepo. A root
package.json whose start script delegates to the right workspace will work:
{
"install": "pnpm install",
"start": "pnpm --filter web start"
}A repository with no root manifest at all, where the app only builds from
apps/web, cannot be configured yet.
You will be able to give each command its own directory, so an app that lives in a subdirectory needs no root wrapper:
{
"install": "pnpm install",
"installDir": ".",
"start": "pnpm start",
"startDir": "apps/web"
}The test file path is recorded from the repository root, so any testDir
works — testDir: "./tests" registers tests/checkout.spec.ts and the deploy
finds it under the clone. What the root rule does still cover is the Playwright
config: the test command runs from the repository root, so keep the config
there and point testDir at wherever the tests actually live.
The two placeholders
The test command needs to know which test to run. Two placeholders are filled
in for you:
{{file}}— the path of the test file that contains the scene call.{{grep}}— the test's full title: every enclosingdescribetitle and the test name, joined with spaces.
For a scene marked inside this test:
test.describe("Checkout", () => {
test("shows the empty cart", async ({ page }) => {
// ...
});
});{{grep}} is Checkout shows the empty cart.
Two things to know if you write your own test command:
Quote {{grep}}. The command runs through a shell, and test titles contain
spaces. -g "{{grep}}" matches the one test you meant; -g {{grep}} hands the
shell a fistful of words.
Use each placeholder at most once. Only the first occurrence of each is replaced.
Everything else about the command is yours. If your tests need a particular Playwright project, or a different runner, say so:
{
"test": "npx playwright test --project=chromium {{file}} -g \"{{grep}}\""
}A worked example
A Next.js app with a Postgres database, where the database runs as a container alongside the app:
{
"install": "docker compose up -d db && npm ci && npm run build && npm run db:migrate && npm run db:seed",
"start": "npm start",
"test": "npx playwright test {{file}} -g \"{{grep}}\""
}Two things make this work. The slow parts — the build, the migration, the seed —
are all in install, which has fifteen minutes to finish, while start has
ninety seconds. And the data the test needs is created by install, because a
deploy starts with nothing but your repository. See
Making your app run inside a deploy.
When it goes wrong
Two errors come from this file, and both stop the deploy before your app runs.
SANDBOX_CONFIG_MISSING — there is no midstream.json in the repository
root at the commit being deployed. A common version of this: the file is on
main but not on the branch that produced the capture.
SANDBOX_CONFIG_MALFORMED — the file is not valid JSON, or install or
start is missing or empty. It has to be plain JSON: no comments, no trailing
commas. Paste it into a JSON validator if you are not sure.
Both are reported on the deploy itself, with the reason. See Why a deploy failed.