When several developers or coding agents use Git worktrees, every checkout can
silently point at the same Convex development deployment. Each convex dev
watcher then pushes its checkout's schema and functions to that shared target.
The last watcher to push wins.
Typical symptoms are:
- A function exists in one checkout but the running app says it does not exist.
- Generated types and the deployed function set disagree.
- A schema appears to change when another worktree saves a file.
- Seed or test data from unrelated branches is mixed together.
The fix is to give each worktree its own short-lived Convex dev deployment and seed it with a small, deterministic baseline.
This folder is a project-neutral reference implementation. Copy it into a Convex repository and adapt the few paths and example tables to your app.
The repository also includes an installable companion skill at
skills/setup-convex-worktrees. The skill inspects an existing Convex
repository, adapts the provisioner to its layout and package manager, designs a
schema-appropriate idempotent seed, adds agent guardrails, and validates the
result.
Install that directory with your Codex skill installer, or copy it into your local skills directory, then invoke:
$setup-convex-worktrees
The skill is an execution layer over this reference implementation. The rest of this README remains the human-readable explanation and manual setup path.
For each worktree:
- Derive a safe deployment reference from the OS user and Git branch.
- Reuse that deployment if it already exists; otherwise create it with an expiration date.
- Select it so the worktree's backend
.env.localpoints at the isolated deployment. - Copy the deployment URL into the frontend env file when the frontend and Convex backend live in different workspace packages.
- Push the checkout's schema and functions with
convex dev --once. - Optionally run an idempotent internal mutation that creates baseline data.
- Run the normal frontend and Convex dev watchers. They now target only this worktree's deployment.
Convex documents the same underlying primitives in its Agent Mode guide and multiple-deployments guide.
.
├── AGENTS.md.snippet
├── LICENSE
├── SOCIAL.md
├── README.md
├── convex
│ ├── schema.snippet.ts
│ └── seed.ts
├── package.json
├── scripts
│ ├── next-dev.mjs
│ └── setup-worktree-convex.mjs
├── skills
│ └── setup-convex-worktrees
│ ├── SKILL.md
│ ├── agents
│ ├── assets
│ ├── references
│ └── scripts
├── tests
│ ├── fake-convex.mjs
│ ├── skill.test.mjs
│ └── setup-worktree-convex.test.mjs
└── worktree-convex.config.mjs
The provisioner uses only Node built-ins. It does not depend on a particular task runner, hosting provider, auth provider, or application schema.
Copy these files to the same relative locations in your repository:
scripts/setup-worktree-convex.mjsworktree-convex.config.mjs
Edit worktree-convex.config.mjs:
export default {
// Directory containing convex.json or the convex/ directory.
convexDir: "packages/backend",
// The env file Convex updates when a deployment is selected.
backendEnvFile: "packages/backend/.env.local",
backendUrlVar: "CONVEX_URL",
// Optional for a split frontend/backend monorepo. Set to null when the
// Convex CLI already writes the correct public URL variable in the same app.
clientEnv: {
file: "apps/web/.env.local",
urlVar: "NEXT_PUBLIC_CONVEX_URL",
},
// Missing, gitignored env files are copied from the primary worktree before
// the deployment binding is replaced in this worktree.
copyFromPrimaryWorktree: [
"packages/backend/.env.local",
"apps/web/.env.local",
],
expiration: "in 14 days",
region: null,
seed: {
functionName: "seed:seedDev",
args: {},
},
// Optional frontend port isolation. Set to null if another tool assigns
// ports or if only the Convex backend needs isolation.
port: {
envFile: "apps/web/.env.local",
envVar: "PORT",
min: 3100,
max: 3999,
localAppUrlVar: "NEXT_PUBLIC_APP_URL",
},
// Optional non-secret values to mirror from a local env file onto each new
// Convex deployment. Prefer Convex project defaults for shared settings.
mirrorToDeployment: [
{
envFile: "apps/web/.env.local",
localVar: "NEXT_PUBLIC_APP_URL",
deploymentVar: "NEXT_PUBLIC_APP_URL",
},
],
};Add a package script:
{
"scripts": {
"worktree:convex": "node scripts/setup-worktree-convex.mjs"
}
}This example uses pnpm:
pnpm worktree:convex --seedThe underlying convex commands work with other package managers too; change
packageRunner in the config if needed.
# Create/select a deployment, push functions, leave data empty
pnpm worktree:convex
# Also run the configured baseline seed
pnpm worktree:convex --seed
# Override the branch-derived slug or expiration
pnpm worktree:convex checkout-name --expiration "in 3 days"
# Configure env files and select the deployment without pushing functions
pnpm worktree:convex --no-pushRe-running the command selects the existing deployment instead of trying to create a duplicate. Its original expiration is not extended.
A fresh deployment is intentionally empty. Avoid cloning an entire shared or production database for routine development: it is slower, can expose sensitive data, and makes tests depend on historical state.
Instead, create the smallest coherent dataset that exercises the app's normal
screens and permissions. The example convex/seed.ts:
- uses an internal mutation, so it is not exposed to browser clients;
- uses stable keys and indexes instead of scanning a table;
- can be rerun without duplicating records;
- records a seed version so future seed changes can be migrated deliberately;
- accepts an optional user identity so auth-linked records can belong to the person testing the worktree.
Merge the example tables and indexes from convex/schema.snippet.ts into your
schema, then replace the example demoItems writes with a valid graph of your
own application data.
Run it directly when needed:
pnpm --dir packages/backend exec convex run seed:seedDev '{}'If the application requires an authenticated owner:
pnpm --dir packages/backend exec convex run seed:seedDev \
'{"ownerUserId":"user_123"}'Keep seed behavior deterministic. A good seed creates valid relationships and permissions, not a large volume of random fixtures.
Automation only works if every agent uses it before starting a watcher that can
push Convex code. Copy AGENTS.md.snippet into the repository's agent
instructions and update the command/path names.
The important rule is:
A linked worktree must never run
convex devwhile it still points at a shared development deployment.
The provisioner is the enforcement mechanism; AGENTS.md makes it the default
behavior.
For stronger containment, Convex can mint a deploy key scoped to the selected deployment:
pnpm --dir packages/backend exec convex deployment token create \
worktree-agent --save-envOnce saved as CONVEX_DEPLOY_KEY, subsequent CLI commands in that worktree are
restricted to the selected deployment. This is especially useful for autonomous
agents. Do this after provisioning; a deployment-scoped key cannot create or
select a different deployment, and the provisioner will fail closed until the
key is unset. Direct convex dev and convex run commands can continue using
the scoped key.
Independent backends are only half the story when several worktrees also run frontend dev servers. The provisioner can assign each linked worktree a stable port and write it to the configured env file.
Some frameworks read PORT automatically. Next.js chooses its port before it
loads .env.local, so use the included wrapper:
{
"scripts": {
"dev": "node scripts/next-dev.mjs"
}
}The wrapper preserves explicit CLI overrides:
pnpm dev
pnpm dev -- --port 4200If the local application URL is http://localhost:3000, the provisioner also
updates it to the assigned port. Non-local URLs are left unchanged.
Convex environment variables belong to deployments; backend functions do not
read the frontend's .env.local. New deployments can receive project-level
default values. That is usually the cleanest place for non-production defaults.
See Convex environment variables.
Use three categories:
- Project dev defaults: ordinary settings every new dev deployment needs.
- Per-worktree mirrored values: non-secret URLs that genuinely differ by checkout, such as a local callback origin.
- Explicit secrets: set through the Convex CLI or dashboard. Do not copy production secrets or scrape another deployment automatically.
If scheduled jobs, webhooks, email, billing, or other side effects exist, make
them inert in temporary deployments unless the feature under test requires
them. A DEPLOYMENT_ENV=development value plus provider sandbox credentials is
often enough.
- Use
deployment create --type dev, never--prod. - Put a finite expiration on every temporary deployment.
- Use argument arrays rather than interpolated shell commands.
- Keep the seed internal, deterministic, bounded, and idempotent.
- Never import production data by default.
- Never copy production provider credentials by default.
- Derive references from both the user and branch to avoid collisions between developers working on similarly named branches.
- Reuse an existing worktree deployment instead of creating one on every run.
- Keep generated Convex types in version control when the repository expects them there.
This reference implementation does not:
- create Git worktrees;
- deploy to production or preview hosting;
- clone production data;
- manage Clerk/Auth0/etc. test users;
- expose local servers over Tailscale or another tunnel;
- supervise long-running processes;
- delete deployments or branches.
Those are separate lifecycle concerns. Keeping provisioning small makes it easy to audit and portable across local machines, CI runners, and agent harnesses.
This workflow expects a recent Convex package with convex deployment create
and convex deployment select; deployment selection by reference requires
Convex 1.34.0 or later. The reference implementation was checked against Convex
1.42.
The included test uses a fake Convex CLI to verify create-versus-select behavior, env propagation, stable port assignment, function pushing, and seed invocation without creating a real cloud deployment:
pnpm testUseful official references: