Portable development environment: three piles, one catalogue and a check
A portable development environment is one a new machine can rebuild without anyone's memory. Sort everything the project needs into three piles: what git holds, what a recipe rebuilds and what you must carry. Write every secret down once, by name, carry the values in one encrypted bundle, and trust the new environment only after a read-only check passes, one that names the fix for every gap.
The catalogue rows, the check output and everything in the worked example are hypothetical, invented to show the method. The measured figures (18 days, a morning, an hour, about 2 GB and ten minutes) come from moving our own agent setup between environments in September and October 2026, and are labelled where they appear. The provider count for Stripe Projects is from Stripe’s documentation.
A project built with Cursor, Claude Code or Lovable usually runs on exactly one machine: the one it was built on. The code is in git. Everything else, the .env file with the keys, the tools that were installed one evening, the database with the test data, the logins, lives on that laptop and in the head of the person who set it up. That works until the laptop is replaced, a developer joins, or the project moves to a cloud sandbox. It is a common place to get stuck, as threads such as where do you actually get stuck when it’s time to ship on r/vibecoding and the ShiftMag piece your .env file wasn’t built for AI agents show. This guide is the method we use so that a move is a routine, not an afternoon of guessing.
Sort what the project needs into three piles
Everything a project needs to run belongs in one of three piles, and each pile reaches a new environment in a different way.
what the project needs to run
:
+----------------------+----------------------+
: : :
GIT HOLDS IT A RECIPE REBUILDS IT YOU MUST CARRY IT
code, config, runtimes, CLIs, secret values, .env,
.env.example, packages, editor logins, database data,
the catalogue setup an agent's memory
v v v
clone build the image one encrypted bundle
| Pile | What belongs in it | Examples | How it reaches a new environment |
|---|---|---|---|
| Git holds it | Anything that is not secret and not generated | The code, .env.example, configuration, the devcontainer file, the catalogue of secret names |
Clone the repository |
| A recipe rebuilds it | Anything you could download again | Node or Python, the Supabase, Stripe and Vercel command-line tools, packages, editor extensions | Build the image or devcontainer |
| You must carry it | Anything that exists only on this machine | The values in .env, logins, local database data, an AI agent’s memory, settings and history, work not yet pushed |
Pack it, encrypt it, move it, delete the copy |
The first two piles are cheap: they rebuild themselves. The third is where moves go wrong, so the goal is to make it as small as possible and to know exactly what is in it.
Two habits shrink it. Push work before a move, so “unpushed work” is empty. And move anything you can download again into the recipe, so it stops being something to carry.
An agent’s state belongs in the third pile even when it looks like clutter. In our own agent setup, the agents’ history is what their limits are checked against. Moving to a bigger machine without it would have started that history at zero, and the limits would have protected nothing.
Write every secret down once, by name
A secret should be written down in exactly one place, by name, never by value. That list is the catalogue. It is the most useful single file in a move, because it turns “what was in that .env?” into a list someone can work through.
| Name | Who needs it | Where to get it | Where it goes |
|---|---|---|---|
SUPABASE_URL |
Laptop, cloud sandbox, host | Supabase project settings | .env of the web app |
SUPABASE_ANON_KEY |
Laptop, cloud sandbox, host | Supabase project settings | .env of the web app |
STRIPE_SECRET_KEY (test) |
Laptop, cloud sandbox | Stripe dashboard, test mode | .env of the web app and of the worker |
STRIPE_SECRET_KEY (live) |
The production host only | Stripe dashboard, live mode | The host’s settings, never a laptop |
OPENAI_API_KEY |
Laptop, cloud sandbox | OpenAI project keys | .env of the worker |
The rows are hypothetical; the columns are the method. “Who needs it” is a role, such as “the build server” or “my laptop”, not a person. It is what stops a personal environment being asked for keys only the build server uses.
Generate the .env files from templates
When we stored each .env file whole, one credential ended up stored twice, and a personal environment was asked for credentials only the build environment needed. An inventory of names, with no values, showed that most of our .env files were placeholders anyway.
The fix was to store each distinct secret once and generate the .env files from templates:
.env.template (in git) STRIPE_SECRET_KEY={{STRIPE_SECRET_KEY}}
worker/.env.template (in git) STRIPE_SECRET_KEY={{STRIPE_SECRET_KEY}}
v
one value, stored once, by name
v
.env, worker/.env (never in git) rendered on each machine
A value used in two files is then changed in one place. A production key stays marked “local only”, because shared secrets reach every environment that is set up from them. When we split our existing .env files into templates and catalogue entries, the split refused any key it could not classify, and rendering the templates back, on copies of the real files, gave files identical to the originals, byte for byte.
Put the tools in the recipe, not in your memory
Every tool someone installed by hand is a step someone will forget on the next machine. Put the tools in a recipe: a devcontainer, which is a short file in the repository describing the machine (runtimes, command-line tools, editor extensions), or a container image built from it. VS Code and cloud sandboxes such as GitHub Codespaces can build the same machine from that file.
We learned where the line sits by getting it wrong twice. A restart of our environment recreated the home directory and silently dropped command-line tools, editor extensions and agent skills that had been installed there. Later, tools were installed at startup onto a persistent disk, which made a brand new environment the expensive case: about 2 GB of downloads and ten minutes before the check was green (measured). The tools moved into the image, and a new environment now starts with them.
| Belongs in the image | Restored when the environment starts | Never in either |
|---|---|---|
| Runtimes such as Node and Python | The project’s clones | A secret’s value |
| Command-line tools: Supabase, Stripe, Vercel, git helpers | Secrets, from the bundle, at the catalogue’s paths | A production key on a development machine |
| System packages | Database data | A personal login baked into a shared image |
| Editor extensions and the agent’s settings |
The middle column is real: some things belong to the person or the project, not to the image, and they are restored each time.
Write the order down as a script
Bringing up a new environment by hand took us a morning (measured). A bootstrap script now does the steps in order, is safe to run twice, and refuses to run in a folder that already holds something a new environment would not have. The steps for most projects are short:
- Build the machine from the recipe.
- Clone the repositories.
- Restore the carried pile from the bundle.
- Generate the
.envfiles from the templates. - Run the check.
Carry secrets and state in one encrypted bundle
The third pile travels as one file, encrypted, and it is deleted once the move has worked. Copying secrets by hand is where moves quietly fail.
One of ours lost an hour (measured) to exactly that. A private key that spans several lines was pasted into a prompt that accepts one line, and arrived as its first line only. A token with write access went into the slot meant for a read-only one. Our checks called both “present”. Only the running system noticed, and the old environment had held the right values all along.
Since then, secrets travel in the bundle. The checklist we follow:
- Before packing, list every checkout with uncommitted or unpushed work, and push it or decide to carry it.
- Stop anything that writes to the state you are carrying, so the database copy is consistent. Our packing step refuses to run while the system is running.
- Encrypt with a passphrase, asked for twice. Never write a plaintext copy, not even for a minute in Downloads.
- On the new machine, decrypt first, so a wrong passphrase restores nothing at all.
- Install only the names the catalogue lists, at the catalogue’s paths, readable by the owner only.
- Refuse to overwrite state that already exists on the new machine.
- Delete the bundle after the move has worked.
- Rehearse the whole thing once in a scratch folder before the real move.
The catalogue is what makes the bundle safe to unpack: anything in it that the catalogue does not name is not installed.
Check the new environment before you trust it
A half-restored environment looks exactly like a working one until you trip over the missing piece. So the last step of every move is a check, and the new environment is not trusted until it passes.
We found out how long “looks fine” can last. Two of our setup scripts wrote a settings file on every rebuild, a running session overwrote both writes within seconds, and both scripts logged success, for 18 days (measured). Nothing failed loudly. The answer was a read-only check, and its first run found a gap nobody knew about. Why a check that finds nothing is not proof of anything is the subject of a guard that does nothing looks like a guard with nothing to do.
We published a small version of this check as open source: envready compares .env with .env.example, checks that .env stays out of git and that the project’s tools are installed, and prints the fix for every failure. The rules for the check:
- Read-only. It changes nothing, so it is safe to run at any time.
- No network, so it cannot fail for a reason outside the machine.
- It never reads a secret’s value. It checks names, paths and permissions.
- One line per check, so a failure is visible at a glance.
- Every failure names the command that fixes it.
- It fails on an empty catalogue. Ours once passed with zero entries.
environment check (hypothetical output)
ok image node, supabase, stripe and vercel CLIs found
ok clones 1 repository, nothing unpushed
ok catalogue 5 names listed, values not read
FAIL .env (web) STRIPE_SECRET_KEY missing fix: restore the bundle
FAIL database local database is empty fix: load the seed data
ok permissions secret files readable by owner only
Present is not correct
A check that a value exists cannot tell you it is the right value. Our checks said “present” for a truncated key and for a token in the wrong slot. So the check has a second half: one real call per service, in test mode, that only the right value can pass. Log in with a test user, read one row from the database, create one test payment. Run those once after the check is green, and the move is done.
Let a service create the accounts when you start fresh
If you are starting a new project rather than moving one, a few services now create the accounts and write the keys for you. We have reviewed these from their documentation; we have not run them in a client project.
| Service | What it does, according to its documentation | What it leaves to you |
|---|---|---|
| Stripe Projects | Creates or links accounts at more than 60 providers, such as Supabase, Vercel, Neon and Clerk, writes the keys into .env, keeps development, staging and production apart, and stripe projects pull rebuilds the project’s keys on a new machine |
It does not write the keys to your production host |
| Vercel Marketplace | Provisions integrations such as databases from the Vercel dashboard and adds their environment variables to the project | Anything outside Vercel, and your local setup |
| AI app builders such as Lovable | Connect Supabase and Stripe to the app for you | Knowing what was connected, and where the keys live |
| 1Password Environments | Serves .env values at runtime instead of leaving them in a file |
Which machine needs which value |
These solve “create the accounts and the keys”. They do not solve “is this machine ready” or “what else has to travel”: the tools, the database data, an agent’s state, unpushed work. Use them for the first pile of keys, and keep the three piles, the catalogue and the check for the rest.
Work through a hypothetical move
Everything in this section is invented. It shows the method on a typical AI-built app, not a result.
Invented inputs
A founder built a booking app in Cursor. It uses Supabase for the database and logins, Stripe in test mode for payments, an OpenAI key for a summary feature, and Vercel for hosting. A developer is joining, and the founder is moving to a new laptop at the same time. The developer will work in a cloud sandbox.
| Input | Hypothetical value |
|---|---|
| Repositories | 1, with a web app and a small worker |
.env files |
2: the web app and the worker |
| Distinct secrets | 5, one of them used in both files |
| Development data | A Supabase project in the cloud, with test bookings |
| Tools installed by hand | Node, the Supabase CLI, the Stripe CLI, the Vercel CLI |
| Unpushed work | One branch on the old laptop |
Walk the three piles
git holds it the code, .env.example, the devcontainer file, the catalogue
a recipe rebuilds it node, supabase CLI, stripe CLI, vercel CLI -> devcontainer
you must carry it 5 secret values, the unpushed branch, the founder's logins
The unpushed branch is pushed first, so it leaves the third pile. The development database lives in Supabase’s cloud, so the data does not need carrying, only the keys to reach it. The tools go into the devcontainer, which both the new laptop and the cloud sandbox build from.
Write the catalogue and the templates
The founder writes the five names into the catalogue, using the table in the second section as the shape. The Stripe test key is used by both the web app and the worker, so it is stored once and both .env files are generated from templates. The live Stripe key stays where it already is, in Vercel’s settings, and is marked as production host only. The developer’s sandbox is listed as needing the test keys and the OpenAI key, not the live key.
Pack, move, check
On the old laptop, the founder packs the five values into one encrypted bundle and moves it to the new laptop. On the new laptop, the devcontainer builds, the bundle is unpacked into the catalogue’s paths, and the templates render the two .env files. The check reports one failure: the worker’s template names a key the catalogue does not list. The fix is one catalogue line. On the second run the check is green, a test login and one test payment go through, and the bundle is deleted.
For the developer’s sandbox, the founder does not send the bundle at all. The developer gets their own development keys from each service, writes them in under the catalogue’s names, and runs the same check. The sandbox never holds the founder’s keys.
On these invented inputs, the move needed one line of fixes. What kept it short was the list, not the tools: every gap had a name before anyone needed it.
Frequently asked questions
Should I commit my .env file?
No. Commit a `.env.example` (or a template) with every name the app reads and a placeholder instead of each value. Git then holds the shape of the configuration, the catalogue says where each value comes from, and the values travel separately. A key that has been committed once should be treated as leaked and rotated, even if the commit is deleted later.
Is an encrypted .env in git, for example with dotenvx, good enough?
It can be, for a small team. Encrypting the file means the values can live next to the code, but the decryption key becomes the one secret you must carry, and everyone who has it can read every value in the file. It also does not tell you who needs which value. Keep the catalogue either way, and keep production keys out of the shared file.
Do I need a secrets manager such as Doppler or Infisical?
Not to start. A secrets manager earns its place when several people and several environments share keys and you need to rotate them in one place. Before that, a catalogue of names and one encrypted bundle for moves covers most of the risk. If you adopt one later, the catalogue is what you load into it.
What about secrets that AI agents can read?
An agent that works in your project can usually read every file in it, `.env` included. Give the machine where agents run only the keys their tasks need, use test keys there, and keep production keys on the production host. Tools that serve the values at runtime instead of leaving them in a file, such as 1Password Environments, shrink what is lying around.
How do I move an app I built in Lovable or another AI app builder?
Connect the project to GitHub if the builder offers it, so the code is in the first pile. Then list what the builder set up for you outside the code: the database project and its keys, the payment account, the hosting, the domain and any email or login service. Those go in the catalogue with where to get each one. Check the builder's own documentation for what an export includes on your plan.
What if I am not technical?
You can still do the most useful part. The three piles and the catalogue are lists, and a spreadsheet is enough: one row per thing the app needs, which pile it is in, and for secrets, the name, who needs it and where it comes from. Hand that to the developer who sets up the next machine. It is the part that usually lives only in someone's head.
How often should I test a move?
Before you need one. Rebuild the project on a fresh machine or a cloud sandbox once, while the old machine still works, and fix everything the check reports. After that, a rebuild whenever the setup changes keeps the recipe honest. A move you have rehearsed is a routine; one you have not is an afternoon of guessing.
Where to start
Take one project this week and list everything it needs to run, one line each, with the pile it belongs in: git, the recipe, or carried. Then write the catalogue of secrets, names and sources only, before the next move rather than during it. If you have an AI-built prototype that a team now has to run and change, [Empes Studio](/services/empes-studio/) takes it to production, and the list is the best thing to bring to the first call.