Skip to content

Repository files navigation

Isolated Convex development deployments for Git worktrees

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.

Use it as a Codex skill

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.

The workflow

For each worktree:

  1. Derive a safe deployment reference from the OS user and Git branch.
  2. Reuse that deployment if it already exists; otherwise create it with an expiration date.
  3. Select it so the worktree's backend .env.local points at the isolated deployment.
  4. Copy the deployment URL into the frontend env file when the frontend and Convex backend live in different workspace packages.
  5. Push the checkout's schema and functions with convex dev --once.
  6. Optionally run an idempotent internal mutation that creates baseline data.
  7. 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.

Included files

.
├── 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.

1. Copy and configure the provisioner

Copy these files to the same relative locations in your repository:

  • scripts/setup-worktree-convex.mjs
  • worktree-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 --seed

The underlying convex commands work with other package managers too; change packageRunner in the config if needed.

Provisioner commands

# 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-push

Re-running the command selects the existing deployment instead of trying to create a duplicate. Its original expiration is not extended.

2. Add an idempotent seed

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.

3. Tell coding agents about the invariant

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 dev while 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-env

Once 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.

4. Isolate the frontend port too

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 4200

If the local application URL is http://localhost:3000, the provisioner also updates it to the assigned port. Non-local URLs are left unchanged.

Environment variables

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.

Safety properties worth keeping

  • 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.

What this deliberately does not include

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.

Current CLI requirements

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 test

Useful official references:

About

Isolated, seeded Convex dev deployments for Git worktrees and coding agents

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages