|
| 1 | +# Project creation |
| 2 | + |
| 3 | +A Project groups environments. An environment is an existing `workspace` record; there is no separate environment table. Every newly created Project starts with an environment. |
| 4 | + |
| 5 | +## Choose the creation flow |
| 6 | + |
| 7 | +| Caller | Flow | Result | |
| 8 | +| --- | --- | --- | |
| 9 | +| New Project onboarding | `POST /api/projects` | Creates a Project and its first environment together, with independently supplied names. | |
| 10 | +| Existing workspace creation UI or caller | `POST /api/workspaces` | Creates a workspace and automatically creates its Project, preserving the existing workspace response. | |
| 11 | +| Create another environment by forking | Existing workspace fork operation | Creates a child workspace in the source workspace's Project. | |
| 12 | + |
| 13 | +Both POST endpoints are internal, session-authenticated APIs. `POST /api/projects` is not a public `/api/v2` endpoint and does not accept API-key principals. This foundation does not remove or deprecate existing workspace creation endpoints. |
| 14 | + |
| 15 | +Do not call both creation endpoints for one onboarding flow: each creates a new workspace and a new Project. Neither endpoint attaches a workspace to an existing Project. A general Project environment-creation endpoint is follow-up work. |
| 16 | + |
| 17 | +## Create a Project and its first environment |
| 18 | + |
| 19 | +Use the shared client and contract for same-origin application calls: |
| 20 | + |
| 21 | +```ts |
| 22 | +import { requestJson } from '@/lib/api/client/request' |
| 23 | +import { createProjectContract } from '@/lib/api/contracts/projects' |
| 24 | + |
| 25 | +const result = await requestJson(createProjectContract, { |
| 26 | + body: { |
| 27 | + organizationId: selectedOrganizationId, |
| 28 | + name: 'Customer support', |
| 29 | + initialEnvironment: { name: 'Production' }, |
| 30 | + }, |
| 31 | +}) |
| 32 | +``` |
| 33 | + |
| 34 | +All three inputs are required: |
| 35 | + |
| 36 | +- `organizationId`: the intended organization's ID, or explicitly `null` for a personal Project. There is no fallback to the session's active organization. The caller must be eligible to create in the requested scope; an ineligible organization request is not silently converted to personal creation. |
| 37 | +- `name`: the Project name, trimmed and limited to 1–100 characters. |
| 38 | +- `initialEnvironment.name`: the first environment's name, also trimmed and limited to 1–100 characters. There is **no default environment name**; `Production` above is an example supplied by the caller. |
| 39 | + |
| 40 | +For personal creation, use the same request with `organizationId: null`. |
| 41 | + |
| 42 | +The HTTP 201 response contains both IDs and names: |
| 43 | + |
| 44 | +```json |
| 45 | +{ |
| 46 | + "project": { "id": "<project-id>", "name": "Customer support" }, |
| 47 | + "initialEnvironment": { "id": "<workspace-id>", "name": "Production" } |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +`initialEnvironment.id` is the workspace ID for existing workspace routes and navigation. |
| 52 | + |
| 53 | +The application operation creates the Project, workspace, Project membership, initial administrator permissions, and starter workflow in one database transaction. A failed transaction leaves none of these new records committed. This endpoint always includes the starter workflow; it has no `skipDefaultWorkflow` option. |
| 54 | + |
| 55 | +Creation uses existing workspace eligibility, billing, and `workspace.create` permission-group rules. Choosing personal scope does not bypass applicable organization permission-group restrictions. This endpoint has no idempotency-key support: resubmitting after an uncertain network result can create another Project, so do not blindly retry. |
| 56 | + |
| 57 | +## Existing workspace creation |
| 58 | + |
| 59 | +Existing callers can continue to use `createWorkspaceContract` and `POST /api/workspaces` with their current body: |
| 60 | + |
| 61 | +```json |
| 62 | +{ |
| 63 | + "name": "Support workspace", |
| 64 | + "skipDefaultWorkflow": false |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +`skipDefaultWorkflow` remains optional and defaults to `false`. The route uses the session's active organization and existing workspace creation policy to resolve ownership and billing. It does not take the explicit Project scope or independent Project name used by `POST /api/projects`. |
| 69 | + |
| 70 | +The workspace and its Project are still created atomically. The generated Project name is `Support workspace - Project`; long names are bounded to 100 characters while retaining the suffix. The response remains `{ "workspace": ... }` with HTTP 200, without a new Project response wrapper. Call `GET /api/projects/by-workspace/[workspaceId]` when an existing workspace caller needs its authorized Project details. |
| 71 | + |
| 72 | +## Server implementation |
| 73 | + |
| 74 | +The Project route calls `createProject` in `application/create-project.ts`. Existing workspace callers continue through their current creation paths. Both use the shared transaction primitive in `lib/workspaces/create.ts`; surface adapters must not independently commit Project and workspace creation. |
| 75 | + |
| 76 | +Project descriptions and Project-scoped files are not part of this creation contract. Project-scoped files and a designated Project brief are follow-up work documented in the foundation plan. |
0 commit comments