> ## Documentation Index
> Fetch the complete documentation index at: https://orbit.nckrtl.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Projects

> How a Project records one repository, its type, its source access, and the source defaults that Instances inherit, and how create, update, and removal work.

# Projects

A Project records one Git repository and the defaults for running it. New Instances inherit its default branch and root. Its type decides what those Instances can do. The API path is `/api/v1/projects`, and the CLI family is [`project`](/docs/cli/project).

## Project Documents

[Project Documents](/docs/reference/project-documents) are a native folder tree of notes and versioned attachments owned by the Project, independent of its Instances and Git branches. Orbit keeps their metadata in the Gateway database and their bodies in a dedicated private UpCloud bucket. The shared contract covers editing, upload/download, archive, permanent removal, and recovery across API, CLI, SDK, MCP, and web.

## Fields

A Project stores these fields. API responses, the SDK, and CLI JSON use the same names.

| Field | Meaning |
| - | - |
| `slug` | Unique name, at most 63 characters. It names the directory of each new checkout and the generated domains. |
| `name` | Display name. It defaults to the slug. |
| `code` | Unique code of three uppercase letters. See [Project codes](#project-codes). |
| `type` | `monorepo`, `laravel-app`, `symfony-app`, `laravel-package`, or `node-package`. See [Project types](#project-types). |
| `repository_url` | HTTPS or SSH Git URL that Orbit uses to fetch. |
| `source_access` | `github_app` or `gh_cli`. How Orbit reads a private `github.com` repository. See [Source access](#source-access). |
| `default_branch` | Branch of the `default` Instance and the base for new branches. |
| `root` | Repository-relative web root that Instances inherit, such as `public` or `apps/site/public`. It is not the checkout path or the Laravel application directory. |
| `task_check` | Optional command that task baselines and handoffs run. It defaults to null for every type. See [Project check](/docs/reference/tasks#project-check). |
| `task_workspace_routed` | Boolean, default true. Whether newly created task workspaces get a Route. See [Task workspace routing](#task-workspace-routing). |
| `review_and_merge` | Boolean, default false. Whether Orbit reviews every push of the Project's tasks, reviews incoming pull requests, and merges reviewed green heads. See [Review and merge](/docs/reference/tasks#review-and-merge). |
| `merge_check` | The check run that must pass on a head before Orbit merges it, such as `Required checks`. Null by default. The flow needs it. |

## Application directory

For a Laravel Instance, the application directory is the effective web root without its trailing `/public` segment. The effective root is the Instance's override when present, otherwise the Project's root. `public` means the checkout root in development, or the release root in production. `apps/site/public` means `apps/site` inside that checkout or release. One shared helper derives the directory for Laravel source inspection and runtime consumers; Orbit does not store a second directory setting.

The application directory contains `composer.json`, `artisan`, development [environment files](/docs/reference/environment-variables#where-the-file-lives), and Laravel [logs](/docs/reference/instance-logs#know-which-file-the-gateway-reads). PHP-FPM, default systemd Instance Processes, and Instance Schedules use it as their application working directory. Production resolves it through `current`. Setup, teardown, deploy steps, and task-check commands still run from the repository root; a nested Artisan step must change directory explicitly. [Setup and teardown](/docs/reference/instance-setup#run-setup) and [task checks](/docs/reference/tasks#project-check) export `VP_HOME` to the Node's resolved Vite+ store, including for project-local `vp`.

Registration never infers a nested root from source files. Configure the Project's root, or send an explicit Instance root override. An Instance can serve more directories of its checkout or release through [Routes with a web root](/docs/reference/routes#serve-several-web-roots). Each one derives its application directory with the same rule and gets its own PHP-FPM pool and `APP_URL`.

## Setup and teardown steps

A Project owns ordered [setup and teardown lists](/docs/reference/instance-setup) for its development Instances. Each named command runs on the Instance's Node from the repository root. A setup step can also name a [Gateway-owned operation](/docs/reference/instance-setup#gateway-owned-setup-steps), such as `@orbit/ci-caches`, which the Gateway renders before the step runs. Production Instances run neither list.

When a command is missing or not executable on that Node (exit 127 or 126), Orbit returns `instance.setup_step_unavailable` or `instance.teardown_step_unavailable` with the step name and `outcome: missing`. The message names the step, Node, and exit code and says the command was not found or is not executable. Other command failures still return `instance.setup_step_failed` or `instance.teardown_step_failed`. See [Instance setup and teardown](/docs/reference/instance-setup#failure-codes) for retry and removal behavior.

## Development deploy steps

A Project owns an ordered [development deploy list](/docs/reference/deployments#development-deploy-steps), separate from setup and teardown and from production's per-Instance deploy steps. Use [`project:dev-deploy-step`](/docs/cli/project#orbit-projectdev-deploy-steplist) to list, create, update, or remove a step. Each step stores a name, command, timeout, and `required` boolean, which defaults to true. These operations change configuration only; they never start a deployment.

## Project types

The type belongs to the Project, so every Instance of one repository behaves the same way.

| Type | Route | PHP-FPM | Root `.` allowed | Default `task_check` |
| - | - | - | - | - |
| `laravel-app` | Exactly one without a web root per active Instance | Yes | No | none |
| `symfony-app` | Exactly one without a web root per active Instance | Yes | No | none |
| `monorepo` | Only an explicit Route | Only with a Route to a Laravel source | No | none |
| `laravel-package` | Only an explicit Route | No | Yes | none |
| `node-package` | Only an explicit Route | No | Yes | none |

`.` means the repository root. A Route cannot target an Instance whose root is `.`. Set a relative web root first. A `laravel-package` Project does not need an `artisan` file.

A `symfony-app` serves like a `laravel-app`: one Route, a PHP-FPM pool, and the PHP version from `composer.json`. Orbit runs no Laravel step for it. It writes no `APP_URL` and patches no Laravel configuration cache. See [Symfony applications](/docs/domains/applications#symfony-applications).

## Create a Project

Create a Project before you create or register its first Instance.

```bash theme={"dark"}
orbit project:create acme laravel-app git@github.com:acme/site.git
orbit project:create acme laravel-app git@github.com:acme/site.git --default-branch=stable --root=web/public
orbit project:create leden laravel-app git@github.com:acme/leden.git --source-access=gh_cli
```

The CLI root defaults to `.` for package types and `public` for other types. The API and SDK require `root`. Without `--default-branch`, the Gateway reads the remote default branch once and stores it. A later change on the remote does not update the Project. An explicit branch must exist on the remote. The Gateway reads the remote with the Project's [source access](#source-access).

A Project's development `default` [deploys in its checkout](/docs/reference/deployments#development-defaults). Its [setup steps](/docs/reference/instance-setup#run-setup) can use that checkout as a dependency seed for other development Instances on the same Node.

Creation is idempotent. A retry with the same values returns the existing Project. A retry that omits the default branch does not read the remote again. A retry with any different value, including another URL for the same repository, returns `project.identity_conflict` and changes nothing.

## Source access

`source_access` decides how Orbit reads a private `github.com` repository. It applies to every read: default-branch checks on create and update, branch resolution on `instance:create`, and every clone and fetch on a Node.

| Value | Orbit reads with |
| - | - |
| `github_app` | The Gateway's [GitHub App](/docs/reference/github-app#how-orbit-reads-a-repository), when an installation covers the repository. Otherwise without a credential. This is the default. |
| `gh_cli` | The [GitHub CLI login](/docs/reference/github-app#read-through-the-github-cli) of the Gateway's `orbit` user. Only for `github.com` repository URLs. |

A public repository and a repository on another host work with `github_app`. A `gh_cli` Project cannot start [tasks](/docs/reference/tasks), because tasks publish through the App.

## Repository identity

The Gateway derives a repository identity from the host and path of the URL. Equivalent SSH and HTTPS URLs, with or without `.git`, have the same identity. A second Project for the same repository returns `project.repository_identity_conflict`. Registration uses this identity to find the Project of a checkout. [Doctor](/docs/cli/doctor#what-each-family-checks) uses it to compare a checkout's origin with the Project, so an SSH origin of an HTTPS Project is not `project.repository_origin_mismatch`.

## Registration needs a Project

[`instance:register`](/docs/domains/applications#register-an-existing-checkout) adopts a checkout only for an existing Project. It finds the Project by repository identity, or uses `--project`. When no Project owns the repository, it fails with `instance.project_missing` and changes nothing. Create the Project with `project:create` first. Registration inherits its root unless an explicit Instance override is sent; it does not search the checkout for nested apps.

SDK Project responses and the `project:list` and `project:show` commands expose the stored type, repository, source access, default branch, root, task check, `task_workspace_routed`, `review_and_merge`, and `merge_check`. The task check is an ordinary setting, like setup steps, so activity records it as sent. The API, SDK, CLI, activity, Doctor, and validation contracts expose no `main_branch` or `--main-branch` compatibility name.

## Retry creation safely

Repeating `project:create` with the same name, slug, type, repository access URL, source access, default branch, root, any sent task check, and any sent `task_workspace_routed` value returns the existing Project. An omitted source access means `github_app`. Omitting `task_workspace_routed` keeps the stored value. A retry does not look up an omitted branch again.

A retry that changes any creation value fails with `project.identity_conflict` and does not mutate the Project. A different repository access URL is a changed value even when it has the same canonical repository identity, so creation never switches the stored URL.

## Project codes

Each Project has a unique code of three uppercase letters. The Gateway derives it from the slug unless `POST /api/v1/projects` sends `code`. The code stays the same when the slug changes. Task cards use it as a label.

Change the code in the web app, or send `PATCH /api/v1/projects/{project}` with only `code`. A code sent with other fields returns `project.code_update_separate`. A code that is not three uppercase letters returns `project.invalid_code`. A code in use returns `project.code_conflict`. When every three-letter code is taken, creation returns `project.codes_exhausted`.

## Update a Project

Use `project:update` when an existing Project must change its display name, type, slug, repository access URL, source access, default branch, relative web root, task check, task workspace routing, or review and merge. The Gateway API accepts `PATCH /api/v1/projects/{project}` with those same fields, including `task_workspace_routed`. The PHP SDK sends `UpdateProjectRequest` to that path. Omitted fields stay unchanged; send `task_check: null` to clear the task check. The CLI and the MCP `project-update` tool accept the same fields. The [Update lifecycle](#update-lifecycle) defines source reconciliation. The API, SDK, CLI, activity, Doctor, and validation contracts expose no `main_branch` or `--main-branch` name.

```bash theme={"dark"}
orbit project:update 3 --repository=https://github.com/acme/site.git --default-branch=stable
orbit project:update 14 --source-access=gh_cli --default-branch=main
```

`project:update` and `PATCH /api/v1/projects/{project}` change the fields you send and leave the rest. Creation never updates a Project.

| Field | Effect |
| - | - |
| `name` and `--name` | Applies at once and touches no checkout, Route, or runtime. At most 255 characters. The slug, domains, and code stay. |
| `type` and `--type` | Applies at once. A `laravel-app` needs a Route; changing away keeps existing Routes. |
| `slug` and `--slug` | Projects every Instance before publication, with no partial projection. Checkout paths, production users, and homes stay unchanged. Generated Routes use the new slug; explicit domains do not. |
| `repository_url` and `--repository` | Runs `git remote set-url origin` in each development checkout and re-binds each production home. Equivalent HTTPS and SSH URLs share an identity. See [Repository changes](#repository-changes). |
| `source_access` and `--source-access` | Applies at once and touches no checkout. See [Change source access](#change-source-access). |
| `default_branch` and `--default-branch` | Must exist on the remote. Switches every development `default` Instance without a `branch_override`. Explicit overrides stay unchanged. |
| `root` and `--root` | Changes the effective root of every Instance without its own root. Orbit reprojects the runtime of each such Instance that has a Route. |
| `task_check` and `--task-check` | Sets the command that task baselines and handoffs run. Send null or `--clear-task-check` to run no check. |
| `task_workspace_routed` and `--task-workspace-routed=true\|false` | Sets routing for future task workspaces. Existing workspaces keep their recorded mode and Routes. |
| `review_and_merge` and `--review-and-merge=true\|false` | Switches the [review-and-merge flow](/docs/reference/tasks#review-and-merge). It applies at the next tick, to open tasks too. |
| `merge_check` and `--merge-check=NAME` | Names the check that must pass before Orbit merges. Send null or `--clear-merge-check` to clear it. |

A type change must keep a valid root. When the stored root is `.` and the new type does not allow it, validation fails on `root`. Send a web root with the type change. A type or root change that leaves a Route target with root `.` returns `route.target_web_root_unsupported`.

A slug change does not move a development checkout. Orbit creates it at `APPS_ROOT/SLUG/NAME` and stores that path. Deployment, source resolution, removal, and Doctor read the checkout's directory from the stored path, not from the current slug. So a checkout created under the old slug keeps working after the change.

### Change source access

The Gateway first resolves the remote default branch with the new `source_access` value. When that read fails, nothing changes. A `default_branch` or `repository_url` sent in the same request is checked with the new value.

### Task workspace routing

`POST /api/v1/projects` and `PATCH /api/v1/projects/{project}` accept `task_workspace_routed` as a JSON boolean. Null, strings, and numbers fail with HTTP 422 `validation.failed`, with details for that field. Creation defaults to true; an omitted update leaves it unchanged. An explicit value participates in creation's identity check; omitting it on an identical retry preserves the existing value. API list and show responses, the SDK, CLI JSON, and MCP expose the stored boolean. CLI human detail output labels it `Task workspace routed`.

The create and update commands accept `--task-workspace-routed=true` or `--task-workspace-routed=false`. An invalid CLI value returns `project.task_workspace_routed_invalid` before a request. The setting controls task provisioning only. It does not change ordinary Instances or bypass root, Route, and Project-type validation. A settings-only update does not reconcile existing sources or Routes.

The migration seeds false for existing Projects with slug `orbit` and true for other existing Projects to preserve their previous creation behavior. This is a one-time migration of the legacy policy; the engine never consults the slug. It also records the mode of existing task workspaces from their actual provisioned state, so Doctor does not reinterpret them after a settings change. Renaming a Project does not change the setting.

### Review and merge

`PATCH /api/v1/projects/{project}` accepts `review_and_merge` as a JSON boolean and `merge_check` as a string of at most 255 characters, or null. The flow is on only while `review_and_merge` is true, `merge_check` is set, and `source_access` is `github_app`. A request that would leave the switch on without one of these fails with HTTP 422 `validation.failed` on `merge_check` or `review_and_merge`, and changes nothing. CLI human detail output labels the fields `Review and merge` and `Merge check`. An invalid CLI value returns `project.review_and_merge_invalid`, `project.merge_check_conflict`, or `project.merge_check_invalid` before a request.

```bash theme={"dark"}
orbit project:update 46 --review-and-merge=true --merge-check="Required checks"
```

[Tasks: Review and merge](/docs/reference/tasks#review-and-merge) describes what the switch changes.

### Update lifecycle

The Gateway applies `slug`, `repository_url`, `default_branch`, and `root` as one recorded operation:

| Status | Work |
| - | - |
| `reserved` | Records the request and the previous values. |
| `preflighted` | Checks every affected checkout, worktree, production home, and generated domain. |
| `prepared` | Switches branches, changes origins, re-binds production homes, and creates replacement Routes. The old values stay in effect. |
| `publishing` | Replaces generated Routes and updates Instance URLs, environments, and runtimes, then publishes the new Project values. |
| `cleaning_up` | Checks that no production Instance changed. |
| `complete` | Done. |

An update is published once a new value is visible: a replacement Route has cut over, or the Project record holds a new value. A failure before that rolls back, also during `publishing`. Orbit withdraws each replacement Route that never activated, renders each environment it synchronized against the old Route again, and restores origins, production homes, branches, and the Project record.

The update then ends in `rolled_back`, and other updates can run. A retry after a fix starts again. A rollback that fails stays `rolling_back`, and an identical retry continues it. A failure after publication stays in place, and an identical retry continues forward.

A different update while one is published but incomplete returns `project.update_in_progress`. An update holds the locks of every Instance of the Project while it runs, so an unpublished incomplete update that another request finds is not running: an update that stopped before this rule, or a rollback that failed. Orbit rolls it back first and then runs the new request. When that rollback fails, the request returns the rollback's error and the old update stays `rolling_back`. A Project without Instances has no Instance lock. Its incomplete update requires an identical retry; a different request returns `project.update_in_progress` without rolling it back.

Slug rollback withdraws only the recorded replacement or a fresh replacement for the same proposed domain. It leaves an unrelated domain change in place.

A slug change synchronizes the stored environment of each development Instance that has one, against the replacement Route. An Instance without stored configuration has nothing to synchronize. Orbit skips that step and records `environment: skipped` with the reason `env.configuration_missing` in the update evidence; Route convergence still writes the new `APP_URL` into the `.env` of the [application directory](#application-directory). See [Project update boundary](/docs/reference/environment-variables#project-update-boundary).

When updating source, the Gateway passes `-c core.hooksPath=/dev/null` and `-c core.fsmonitor=false` to Git during preflight, origin changes, fetches, branch changes, and rollback. The Gateway does not run checkout hooks or a custom filesystem monitor for those operations. The overrides do not change the stored Git configuration.

Branch switch, rollback, and origin changes run as the Node's managed user, the owner of the checkout. Branch switch and rollback checkout commands write the work tree, so they run without a credential environment. A switch checks out with `--no-track`, and then records the branch's upstream. Preflight fetches use the Project repository URL and never read the checkout's configuration; see [credentialed Git](/docs/reference/instance-setup#credentialed-git).

Run [Doctor](/docs/cli/doctor) to inspect any projection that needs attention.

### Repository changes

A repository change touches only `origin`. Local branches, the checked-out commit, and the recorded starting commit stay the same. Orbit never pushes. Linked worktrees share the checkout's repository and need no change.

Every origin check reads the `remote.origin.url` stored in the checkout. It ignores `insteadOf` rewrites on the Node.

#### Production Instances

A production home records the repository URL in three places: the release layout marker and the initial clone marker under `/var/lib/orbit/app-instance-sources/<instance>/`, and `remote.origin.url` of each retained release. [Deployment and rollback](/docs/reference/deployments) compare these values byte for byte with the Project's `repository_url`.

A repository change re-binds every production home on the release layout to the new URL. Preflight reads each home first. Every recorded URL must name the same repository as the new URL, in SSH or HTTPS form, with or without `.git`. Another repository fails with `project.production_repository_mismatch`, and nothing changes. Prepare then rewrites each value that differs, the releases first and the release layout marker last. Each rewrite is a compare-and-swap, so a retry continues safely. A partial release without an origin stays as it is. Rollback writes the recorded URLs back.

A re-bind changes only those URLs. It never changes production source, the selected release, the deployment branch, or the recorded commit, and it never starts a deployment.

A repository request also re-binds when the URL is already the Project's URL. Use it to repair a home that an update before this rule left on the old URL. Deployment names that repair with `deployment.repository_rebind_required`.

```bash theme={"dark"}
orbit project:update 3 --repository=https://github.com/acme/site.git
```

## Remove a Project

`project:destroy` removes a Project that has no Instances and no Routes. It deletes the Project's process and Schedule definitions, [task definitions](/docs/reference/tasks#task-definitions), setup and teardown steps, Node exclusions, and update records.

A Project with tasks cannot be removed. The Gateway refuses the request with HTTP 409 and `project.has_task_groups`.

## Errors

The Gateway returns these codes for Project requests.

| Code | Cause |
| - | - |
| `project.identity_conflict` | A create retry differs from the stored Project. |
| `project.repository_identity_conflict` | Another Project owns the repository. |
| `project.default_branch_unavailable` | The Gateway cannot read the remote, or the branch is missing. The message names the App or the GitHub CLI login. It holds no Git output or credentials. |
| `github.cli_unauthenticated` | A `gh_cli` read found no `gh` on the Gateway, or no `github.com` login for the `orbit` user. |
| `project.slug_conflict` | Another Project has the slug. |
| `project.update_required` | The update sends no field. |
| `project.update_in_progress` | Another update of this Project is published but incomplete. Repeat that update to finish it. |
| `project.repository_preflight_failed` | A checkout is missing, has another origin, or cannot reach the new URL. |
| `project.repository_origin_failed` | Orbit could not change or restore `origin` in a checkout. |
| `route.domain_conflict` | A new generated domain for the slug belongs to another Route. |
| `project.repository_unowned_common` | A worktree uses a repository that no Orbit checkout owns. |
| `project.source_switch_failed` | A `default` checkout cannot switch to the new default branch. |
| `project.production_ownership_changed` | A production Instance changed during the update. |
| `project.production_repository_mismatch` | A production home records another repository than the new URL. `details.instance_id` names the Instance. |
| `project.production_rebind_failed` | Orbit could not read or re-bind a production home. |
| `project.update_failed` | The update failed for a reason without its own code, and Orbit rolled back. |
| `project.has_instances` | Removal found Instances. |
| `project.has_routes` | Removal found Routes. |

## Why it works this way

These reasons explain the design. Check them before you propose a change.

### One repository, one Project

Registration must find exactly one Project from a checkout's origin. Comparing URLs as strings was rejected, because an SSH and an HTTPS URL would allow two Projects for one repository. Letting Projects share a repository and taking the first match was rejected, because the result would depend on database order.

### One way to create a Project

`project:create` is the only way to create a Project, so every field, such as source access, is set in one place. Creating a Project during registration was rejected. That second path needed its own options, prompts, and conflict codes, and could not set every field. A private repository that needs the GitHub CLI failed there before the operator could choose the setting.

### Updates are separate from creation

Creation stays idempotent: a changed value is a conflict, never an update. A slug, URL, branch, or root change touches checkouts, Routes, and runtimes on several Nodes, so it runs as one recorded operation. A plain database update would leave checkouts, domains, and document roots out of step.

### Recovery goes forward after publication

Until a replacement Route cuts over or the Project record changes, the old values are still in effect, so a rollback is safe. After that point, clients can already see the new domain. Rolling back would change a public name twice. So a failure after publication is retried forward. Deciding by status alone was rejected: an update in `publishing` that failed before its first cutover kept the old domain live, yet blocked every other update and left its replacement Route and origin changes behind until someone repeated the exact request.

An unpublished update that stopped is rolled back when a different request arrives, instead of returning `project.update_in_progress`. The Instance locks prove that it is not running, and rolling it back changes nothing that clients see. Refusing would leave the operator with no supported way out when the stopped request keeps failing.

### Type decides capabilities

Instances of one repository share one serving contract. Per-Instance route or PHP-FPM flags were rejected. A Laravel package or a monorepo must not publish a domain or keep an idle PHP-FPM master, so only `laravel-app` and `symfony-app` get a Route by default.

### A setting routes task workspaces

A new task workspace is visitable only when the Project's `task_workspace_routed` setting says so. Choosing that from the slug `orbit` was rejected, because the engine would then know one repository. [Task workspace routing](#task-workspace-routing) records the one-time migration of that old result, and that a later change does not reroute a workspace that already exists.

### One public name without compatibility

Project is the only public name for the repository record. Orbit has one operator, who does not value legacy support, so compatibility paths, aliases, inert endpoints, and conversion windows are removed by default without waiting for fleet migration or another confirmation. A second name adds code, tests, documentation, and ambiguity without protecting a supported user population.

The supported surface is `/api/v1/projects`, `project:*`, the Project MCP tools, and Instance fields `project_id` and `project`. The former `/api/v1/apps` routes, their nested Process and Schedule definition routes, and generated `app-*` MCP tools are removed. Project creation requires an explicit `type`; there is no path-specific default for an old endpoint. CLI Project selection uses `--project`, not `--app`. [Dependency scan and update](/docs/reference/instance-dependencies) select one Instance by its Route domain with `--project`.

The same rule applies beyond the repository record: `--wireguard-ip` has no `--wireguard-address` alias, and the [task driver settings](/docs/reference/tasks#drivers) use separate implementer and reviewer environment variables, with no `ORBIT_TASKS_AGENT_DRIVER` fallback. Older clients that depend on removed names must be updated; Orbit provides no compatibility period.

Retaining `/apps` until clients migrate is rejected, because there is no other user's migration to protect. Keeping aliases because they seem cheap or harmless is rejected, because they obscure the supported interface and still need maintenance. A conversion window or an inert endpoint is rejected, because neither provides value to this operator. Public compatibility cleanup and [stored record naming](#project-and-instance) are separate decisions; coupling them would mix interface removal with database and class renaming. Unrelated names such as the GitHub App and Node roles are not compatibility aliases for Project.

### Project and Instance

Project is the repository record. Instance is one running copy of a Project on a Node. Those are the only names for this domain: the model, the table, the foreign key, the class, the API, the CLI, and these docs. Nothing keeps a second name as an alias, a route, a JSON field, a class, a table, or a column.

The model for the repository record is `Project`, and the model for the running copy is `Instance`. Tables are `projects`, `instances`, and `instance_*`. Project update rows are `project_updates`. Foreign keys are `project_id` and `instance_id`. A path parameter that identifies a Project is `{project}`.

Project actions live in `Actions/Projects`. A class is `Project*` or `Instance*` according to whether it belongs to the repository record or the running copy. The Gateway, the CLI, the PHP SDK, and the web app follow that rule. Laravel's root namespace stays `App\`, so those actions are `App\Actions\Projects`.

Environment placeholders are `{{instance.domain}}` and `{{instance.environment}}`. A stored value that contains any other `{{...}}` placeholder is invalid. Synchronization returns `env.reference_unavailable`. A write that fails placeholder validation returns `env.configuration_invalid`. The migration rewrites stored placeholders to these two names. Project has no morph alias. The public subject type is `project`. The Instance morph alias is `instance`. The migration rewrites stored activity class names to `App\Models\Project`.

Realtime types are `project.created`, `project.updated`, and `project.deleted`. Payload classes are `ProjectData`, `InstanceData`, and `InstanceDeploymentData`. Project error codes use the `project.` prefix. `project.has_instances` means removal found Instances. `node.has_instances` means the Node still has Instances. `instance.placement_unavailable` means placement failed. Doctor's repository family is `project`.

Monorepo folders such as `apps/cli` and `apps/gateway`, the Laravel root namespace `App\`, `app/` source folders, `config/app.php`, `APP_*` variables, Node roles `app-dev` and `app-prod`, the GitHub App, the type `laravel-app`, the Route kind `app`, and the node storage setting `apps` are not this domain.

Keeping a stored name that differs from Project or Instance was rejected, because every reader would translate. An alias that accepts another name was rejected, because an alias is a second supported path.

Renaming the monorepo folders, the Laravel root namespace, the `app/` source folders, `config/app.php`, the `APP_*` variables, Node roles, the GitHub App, the type `laravel-app`, the Route kind, or the node storage setting was rejected, because those names are not the Project or Instance record.

Reading a stored placeholder or activity class under a different spelling, as if it were the current name, was rejected: that reading is an alias, and synchronization and activity resolution then fail on rows that use the other spelling. The migration renames tables and columns in place. There is no compatibility view and no dual-write. A caller that sends a removed field, or code that imports a removed class, fails. There is no compatibility period.

A repository check fails when an App-domain name is present in app code, database code, the SDK, the CLI, web sources, or these pages. A monorepo path under `apps/` is not that name. A `covers:` glob names a file that exists, and the check rejects an App-domain name left in that glob. The same check rejects the Node names the web app used before [Concepts](/docs/concepts) settled on Nodes with roles and roleless Nodes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.