Ploy
Ploy
Features

Monorepos

Deploy multiple Ploy projects from one repository and share local emulator state across them.

Monorepos

Ploy detects monorepos automatically — there is no monorepo: true flag. A repository is treated as a monorepo when it contains workspace indicators (pnpm-workspace.yaml, package.json with workspaces, turbo.json, etc.) or when more than one ploy.yaml is present.

Each ploy.yaml becomes its own deployable project. You can keep an apps/api, an apps/web, and a packages/worker-jobs in the same repo, deploy them as three independent Ploy projects, and share resources (databases, queues, workflows) between them by pointing them at the same resource name.

Layout

my-app/
├── ploy-workspace.yaml        # optional — workspace-level config
├── apps/
│   ├── api/
│   │   └── ploy.yaml          # → project "api"
│   └── web/
│       └── ploy.yaml          # → project "web"
├── packages/
│   └── worker-jobs/
│       └── ploy.yaml          # → project "worker-jobs"
├── package.json
└── pnpm-workspace.yaml

Each ploy.yaml is a complete, self-contained config. There is no merge with a parent file. Project names default to the parent directory; override with the name: field if you need something different.

apps/api/ploy.yaml
kind: worker
name: api # optional — defaults to "api"
build: pnpm build --filter api
db:
  DB: default # shares the "default" DB with any other project that names it
queue:
  TASKS: tasks

Sharing resources between projects

Resources are addressed by their resource name (the lowercase string on the right side of the binding map). Two projects that bind to the same resource name read and write the same underlying resource — locally and in production.

apps/api/ploy.yaml
db:
  DB: default
apps/web/ploy.yaml
db:
  DB: default # same DB as apps/api

To keep separate resources, use distinct names:

# apps/api/ploy.yaml
db:
  DB: api_db

# apps/web/ploy.yaml
db:
  DB: web_db

Only one project may own migrations for a shared resource. If two projects both ship migrations/DB/*.sql for the same resource name, Ploy errors out with both project names so you can pick a single owner.

ploy-workspace.yaml

A workspace config at the repo root is optional. Add one when you need to:

  • Exclude ploy.yaml files that aren't deployable (e.g., examples, fixtures, templates)
  • Set workspace-level dev port ranges
  • Pin the local dashboard port
ploy-workspace.yaml
exclude:
  - examples/**
  - templates/**
ports:
  worker:
    from: 8800 # auto-allocate worker ports starting here
dashboard:
  port: 9787

node_modules/ is always excluded by default.

Local development

Run ploy dev from the repo root to bring up all projects together:

$ ploy dev
Ploy workspace dev
  Dashboard: http://localhost:9787
  api  worker  http://localhost:8787
  web  worker  http://localhost:8788

What this gives you:

  • One shared .ploy/ directory at the repo root — all SQLite DBs, file storage, and emulator state live here. Resources with the same name across projects share the same files, exactly as in production.
  • One shared dashboard at http://localhost:9787 exposing every project's bindings and the resources they touch.
  • One workerd per project on auto-allocated ports starting at 8787. Override per-project with dev: { port: 8800 } in that project's ploy.yaml.
  • Single Ctrl-C stops everything cleanly.

To run a subset:

$ ploy dev --project=api
$ ploy dev --project=api,web

Running ploy dev from inside a single project's directory still works the old way: that project alone is started with its own .ploy/ and dashboard. This preserves backward compatibility for repos that aren't yet set up as workspaces.

Cloud deployments

When you connect a GitHub repository, Ploy scans it for ploy.yaml files (using the GitHub API — no clone needed) and shows the detected list in the dashboard. You select which entries to enable as projects. Each row defaults its name to the parent directory; override inline.

After the initial connect, pushes to the default branch are diffed against the enabled set:

  • A ploy.yaml that already maps to a project triggers a build for just that project.
  • A new ploy.yaml that wasn't previously enabled appears as a "new project detected" entry. Nothing deploys until you explicitly enable it.
  • A ploy.yaml that's removed from the repo leaves the corresponding project untouched (archive or delete it manually if no longer needed).

This means adding a deploy target to an existing monorepo is just git add apps/billing/ploy.yaml && git push — Ploy will surface the new project for confirmation rather than auto-deploying.

How base interacts with monorepos

In a monorepo, the build runs from the project's directory (the directory containing its ploy.yaml). Dependencies are installed from the repo root so workspace packages resolve. You don't need to set base: — it's inferred from the ploy.yaml location.

The base: field is only required if your ploy.yaml lives at the repo root but the deployable code lives in a subdirectory. In that case base: tells the builder where to run the build:

ploy.yaml at repo root
kind: nextjs
base: apps/web
build: pnpm build --filter web

For per-project ploy.yaml files in subdirectories, omit base: — it's inferred.

Migration from monorepo: true

The monorepo: true flag has been removed from ploy.yaml. Remove it from your config files; Ploy now detects monorepos automatically from workspace indicators in your repo.

If you previously had:

apps/web/ploy.yaml (old)
kind: nextjs
base: apps/web
monorepo: true
build: pnpm build --filter web

Update to (drop both monorepo and base — the latter is inferred from the file's location):

apps/web/ploy.yaml (new)
kind: nextjs
build: pnpm build --filter web

A validation error will point out any remaining monorepo: keys when you next run ploy build or push to a connected repo.

How is this guide?

Last updated on