Profiles
Profiles are Zapper's primary runtime selection model. They combine env file selection, service selection, and stack isolation into one named configuration.
Profiles are Zapper's primary runtime selection model. They combine env file selection, service selection, and stack isolation into one named configuration.
Model
Profiles describe how a stack runs.
profiles:
default:
env_files: [.env.local, .env]
services: "*"
isolate: false
e2e:
env_files: [.env.local, .env.e2e, .env]
services: [thing, thang, thong]
isolate: trueA profile answers three questions:
- Which env files are loaded?
- Which services are active?
- Should this profile run as an isolated stack?
This replaces the legacy product shape where users had to understand and coordinate separate environment sets, service profiles, and instances.
Legacy Model
Before the profile-first migration, Zapper had three separate selectors:
- Root
env/env_filescan be a named map of env file sets. - Service-level
profilesdecide whether individual services are enabled. - Instances decide which runtime namespace a command controls.
Each selector is useful, but together they create too much surface area:
zap env proddata
zap profile api
zap up --instance e2eThe product problem is not that any individual concept is wrong. The problem is that users must compose them manually, and automation must avoid sticky state across multiple axes.
Profile Model
The user-facing model is:
profile = env files + service selection + isolation behaviorExamples:
zap profile set default
zap up
zap --profile e2e up
zap --profile e2e downSticky selection remains useful for humans:
zap profile set proddata
zap upExplicit flags are the expected automation shape:
zap --profile e2e up
zap --profile e2e test
zap --profile e2e downAn explicit --profile flag overrides the selected profile for that invocation
without mutating saved state.
Config Shape
The top-level schema is:
project: myapp
profiles:
default:
env_files: [.env.local, .env]
services: "*"
isolate: false
proddata:
env_files: [.env.local, .env.proddata, .env]
services: "*"
isolate: false
e2e:
env_files: [.env.local, .env.e2e, .env]
services: [api, worker, postgres, browser]
isolate: true
native:
api:
cmd: pnpm dev
env: "*"
worker:
cmd: pnpm worker
env: "*"
docker:
postgres:
image: postgres:15
env: "*"env_files
env_files is the profile's root env file stack.
Files are resolved relative to zap.yaml. Later files override earlier files.
services
services selects the services that belong to the profile.
Allowed forms:
services: "*" # All configured services
services: [api, worker, db] # Only named servicesThe service list uses canonical service names. Aliases remain command-line convenience names, not config identity.
When a selected service declares depends_on, its dependencies are included in
the active profile automatically, even if they are not listed directly. For
example, services: [api] also includes postgres when api has
depends_on: [postgres].
isolate
isolate controls whether the profile gets its own stack.
isolate: falseThe profile applies to the default stack in this local copy. Switching between non-isolated profiles changes what the current stack should look like.
isolate: trueThe profile runs as a separate stack in this local copy and can coexist with non-isolated profiles.
Isolation Behavior
The important product distinction is:
- Non-isolated profiles are modes of the same local stack.
- Isolated profiles are separate running copies of the stack.
For example:
profiles:
default:
env_files: [.env.local, .env]
services: "*"
isolate: false
proddata:
env_files: [.env.local, .env.proddata, .env]
services: "*"
isolate: false
e2e:
env_files: [.env.local, .env.e2e, .env]
services: [api, worker, postgres, browser]
isolate: trueSwitching profiles compares each service's resolved environment and definition before changing services. It stops excluded and changed services, then starts the target's required services and waits for configured health checks. Shared services with unchanged definitions and environment values keep running, even when the env-file selection changes. Added or removed environment variables also count as changes. Each affected service is replaced once in this transition. When switching to or from an isolated profile, the previous profile's services stop before the target stack starts. The saved selection changes only after the transition succeeds.
To run an isolated stack alongside the selected stack, use zap --profile e2e up.
This one-command override leaves the saved selection and other stacks alone.
Local Copies, Stacks, and Stack IDs
Use precise vocabulary:
local copy = one checkout / project root on this machine
profile = named way to run the stack
stack = one running namespace produced by a profile
stackId = internal/random ID for that stackA machine can have multiple local copies of the same Zapper project. Each local copy can have multiple stacks if isolated profiles have been used.
Example:
/Users/me/app default profile -> stackId abc123
/Users/me/app e2e profile -> stackId def456
/Users/me/app-copy default profile -> stackId ghi789
/Users/me/app-copy e2e profile -> stackId jkl012For non-isolated profiles, the stack is the shared default stack in the local copy. For isolated profiles, the stack is profile-owned.
Normal command output should avoid opaque IDs:
== MyProject ==
== MyProject [e2e] ==
== MyProject [abc123 - 3 stacks] ==
== MyProject [e2e - abc123 - 3 stacks] ==If a user needs the internal ID, use the explicit stack command instead of
ordinary headers. Ordinary status output should only include the stackId when
it disambiguates multiple known stacks:
zap stack id
zap stack current
zap stack listJSON and extended/debug output can include stackId, profile, project root,
and local-copy count.
State File
The .zap/state.json file uses stack vocabulary:
{
"selectedProfile": "default",
"stacks": {
"default": {
"stackId": "abc123",
"profile": "default",
"ports": {
"API_PORT": "54321"
},
"volumes": {}
},
"e2e": {
"stackId": "def456",
"profile": "e2e",
"ports": {
"API_PORT": "61234"
},
"volumes": {}
}
},
"lastUpdated": "2026-05-19T00:00:00.000Z"
}selectedProfile is sticky human state. stacks.default is the shared
non-isolated stack. Isolated profiles get their own stack entries, normally
keyed by profile name. stackId replaces the old random instance ID term.
For a non-isolated profile switch, the default stack records the profile that is currently materialized there:
{
"selectedProfile": "proddata",
"stacks": {
"default": {
"stackId": "abc123",
"profile": "proddata",
"ports": {},
"volumes": {}
}
}
}The global registry tracks local copies across the machine. Local
.zap/state.json tracks stacks inside the current local copy.
CLI Shape
Primary commands:
zap profile
zap profile list
zap profile get
zap profile set <name>
zap profile resetzap profile opens an arrow-key picker with the active profile marked and
selected. Enter closes the picker and shows service progress, then a success
message and exits. Escape or Ctrl+C cancels the picker without changing services.
For scripts, get prints only the profile name and set <name> waits for the
transition. current and use remain aliases. reset applies the default profile
with the same service transition. Excluded services stop automatically; the old
--force flag is accepted for compatibility.
Without a terminal, use get or set. Bare zap profile --json continues to
report the current profile. A failed transition exits with an error and retains
the previous saved selection; services may have partially changed, so fix the
reported error and retry the switch.
Stack-targeting commands accept --profile:
zap --profile default up
zap --profile proddata restart
zap --profile e2e status
zap --profile e2e logs api
zap --profile e2e downA profile is the named env/service/isolation selection; a stack is the runtime namespace produced by that selection.
Rules:
zap profile set <name>updates saved state.zap --profile <name> <command>overrides saved state for one invocation.--profiledoes not mutate saved state.- Stack commands show the selected profile in human output when useful.
- JSON output includes the resolved profile name and stack metadata.
Advanced instance flags can remain internal or be reintroduced later, but the main docs do not require users to learn instances.
Migration
No backward compatibility is required for this migration.
Environment sets
Before:
env:
default: [.env.local, .env]
proddata: [.env.local, .env.proddata, .env]After:
profiles:
default:
env_files: [.env.local, .env]
services: "*"
isolate: false
proddata:
env_files: [.env.local, .env.proddata, .env]
services: "*"
isolate: falseBefore:
zap env proddata
zap upAfter:
zap profile set proddata
zap upFor automation:
zap --profile proddata upService-level profiles
Before:
native:
api:
cmd: pnpm dev
profiles: [dev, e2e]
browser:
cmd: pnpm test:browser
profiles: [e2e]After:
profiles:
dev:
env_files: [.env.local, .env]
services: [api]
isolate: false
e2e:
env_files: [.env.local, .env.e2e, .env]
services: [api, browser]
isolate: true
native:
api:
cmd: pnpm dev
browser:
cmd: pnpm test:browserService membership moves out of each service and into the profile definition. This makes each profile readable as a complete stack mode.
Instances
Before:
zap up --instance e2e
zap down --instance e2eAfter:
profiles:
e2e:
env_files: [.env.local, .env.e2e, .env]
services: "*"
isolate: truezap --profile e2e up
zap --profile e2e downInstances become an implementation detail of isolated profiles.
Implementation Notes
Zapper does not run the old and new product models side by side. The old surface-level selectors were removed and the reusable env/service/stack mechanics were wired into the profile model.
The runtime invariant is:
Every stack command runs against a resolved profile.A resolved profile contains:
{
name: string;
envFiles: string[];
services: "*" | string[];
isolate: boolean;
}Runtime names, native process names, Docker names, and name parsing are core Zapper mechanics and stay outside the profile migration except where existing runtime code consumes the selected profile.
Current Status
- Top-level
profilesare supported inzap.yaml. - Root named env sets are removed; root
envandenv_filesare file stacks. - Service-level
profilesare removed. zap env <service>inspects resolved environment variables and no longer switches state.zap profile set <name>updatesselectedProfile.zap --profile <name> <command>is a one-command override and does not mutate state.- Isolated profiles create or reuse profile-owned stacks.
zap stack id,zap stack current, andzap stack listexposestackIdexplicitly.- Normal status headers avoid opaque IDs unless they disambiguate multiple known stacks.
Defaults
- Missing
profileskeeps the historical single-stack behavior with rootenv/env_files. - Profile
env_filesdefaults to an empty stack. - Profile
servicesdefaults to"*". - Profile
isolatedefaults tofalse. - Use
services: "*"for ordinary local development profiles. - Use
isolate: truefor E2E, CI, PR, and destructive test profiles. - Keep instance terminology out of the happy-path docs. Prefer local copy, profile, stack, and stackId.