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

# Create a project

> Carve a supervisor into a tenant slice — a namespace with its own pod, egress and LoadBalancer addresses, isolated by default.

By the end of this page one team has a namespace of their own on the supervisor, with its own slice of the address plan, isolated from every other project unless you said otherwise — and you will have verified all three rather than assumed them.

This page starts where [Platform onboarding](https://docs.celum.io/get-started/platform-onboarding.md) ended: Networking is done, so there is an address plan to carve from. [Projects](https://docs.celum.io/concepts/projects.md) explains what a project *is*; this is how you make one.

## What this changes on your cluster

A namespace, and the Cilium objects for whichever slices you asked for: a `CiliumPodIPPool`, a `CiliumLoadBalancerIPPool`, one or more `CiliumEgressGatewayPolicy` objects, a `CiliumNetworkPolicy` when isolation is on, and a Service that advertises the egress address. An isolated project also gets a gateway of its own by default: `Gateway <name>-envoy` and its `EnvoyProxy`, with a VIP from the project's LB pool.

**No row is written to Celum's database.** A project lives entirely in Kubernetes, which is why the supervisor is also where its address book is read from: the set of taken CIDRs is computed from the live cluster on every create, not from a ledger that could drift.

```mermaid theme={null}
flowchart LR
    B[Supervisor address plan] --> P1[project a]
    B --> P2[project b]
    P1 --> N[namespace proj-a]
    P1 --> C[pod pool, LB pool, egress policy]
    P1 --> I[isolation policy]
```

## Before you start

* **An onboarded supervisor.** Cilium must be running; the objects above are all Cilium CRDs.
* **The supervisor's pool config must be seeded.** Without it `POST` answers **412** — *"supervisor pool config not set — configure pod supercidr (Cilium) + egress pool (NuviraClusterConfig) first"*. This is the supervisor-wide address plan every project is carved from, and it is set once per supervisor.
* **Permissions**: `project:GetConfig` to read the plan, `project:List` to see what is taken, `project:Create` to make one. All are scoped `supervisor:<name>:project:*`.

> [!WARNING]
>
> `GET /api/supervisors/{name}/projects/config` **always answers 200**, even when nothing is configured. The `seeded` field is what tells you apart: `true` means the `NuviraClusterConfig` CRD exists and you are reading the real plan, `false` means you are reading derived defaults — a Cilium pod CIDR and a fallback egress pool — that no one chose. A `false` here is why an otherwise sensible-looking create returns 412.

## The shape of it

**Read what is free → pick the slices → create → verify it took.** The first and last steps are the ones people skip, and both failure modes are quiet.

## Step 1 — Read the address plan and what is already taken

> [tab] Dashboard
>
> Projects → **Create project**. The form reads both for you and offers the next free slice.

> [tab] API
>
> `GET /api/supervisors/{name}/projects/config` for the plan — pod supercidr and mask, egress pool, LB supercidr and slice mask, VM-identity supercidr and slice mask. Then `GET /api/supervisors/{name}/projects` for what is spoken for.

> [tab] MCP
>
> `get_project_networking_config` and `list_projects`.

![The Create project dialog with Identity fields Name, Description and Labels, and choice cards for Pod CIDR (Default, Specific CIDR), LB pool (Default auto-allocating the next free /26, Specific CIDR) and VM identity (None, Specific CIDR), each with an Advertise via BGP switch](https://mintcdn.com/celum-e0119be0/sA7S2Ajgv1l2CH1V/images/get-started/project-create.png?fit=max\&auto=format\&n=sA7S2Ajgv1l2CH1V\&q=85\&s=7fad86a8ab18cab3d8011dc7f3d300b0)

Create project: each slice offers its default or a range of your own, and says where the default comes from

**Result:** `seeded: true`, and a list of existing projects with their `podCidr`, `lbCidr` and `egressIp`. Those are the ranges you must not pick.

## Step 2 — Choose the slices, and know what empty means

Five decisions, and **an empty value does not mean the same thing for each of them.** An empty pod CIDR or egress address inherits: nothing is created for the project. An empty LB range gets the next free slice of the LB band. An empty VM-identity range is decided later, when the first VM asks for an identity address. Read the second column before you leave a field empty.

| Field | Empty means | Set it when |
| - | - | - |
| `podCidr` | pods use the cluster's default pool | the tenant needs pod addresses that are theirs, routable or accountable on their own |
| `egressIp` | pods egress as their node, through ordinary masquerade | traffic leaving this project must be identifiable at a firewall by address |
| `egressNodeCount` > 1 | — | you want one egress IP *per candidate node* instead of a single shared one. Replaces `egressIp` |
| `lbCidr` | Celum allocates the next free slice of the LB band (`lbSupercidr`, cut into `lbSliceMask`-sized slices). If the supervisor declares no LB band, the project gets no LB pool | the tenant's published services need a range that you choose |
| `vmIdentityCidr` (/16–/29) | no pool yet. The first VM in the project that asks for an identity address takes it from a supervisor-wide VM-identity pool if one exists. Otherwise Celum carves the next free slice of the VM-identity band for the project. With neither a pool nor a band, the request is refused | you want to choose the range that each VM's routable /32 comes from. The address follows the VM through live migration, and no LoadBalancer is needed |

> [!NOTE]
>
> Empty used to mean auto-allocate for the pod CIDR too. That changed on 2026-05-25: every project carved a pod slice whether it needed one or not, and the pod supercidr ran out after 256 projects. The LB slice kept the old behaviour, so every project created without `lbCidr` takes a slice of the LB band. When the band has no free slice left, the create is refused with **409**.

Three switches default to **on**. The first two are omitted-means-true, and the third follows the first:

* **`defaultIsolationPolicy`** — a new project is isolated unless you opt out. It creates a default-deny `CiliumNetworkPolicy` in the namespace.
* **`observabilityEgress`** — whether that isolation policy opens egress to the monitoring endpoints. It is only meaningful when isolation is on, since with no policy there is nothing to gate. **Turning it off silently breaks every guest agent's metrics and log shipping**, which surfaces later as an empty dashboard rather than as an error here.
* **`dedicatedGateway`** (Dedicated Envoy in the form) — omitted, it follows isolation: an isolated project gets an Envoy gateway of its own wherever the supervisor can run one, and a line in `warnings` where it cannot. `false` keeps the project on the shared gateway; `true` is refused with **409** where it cannot work. See [Projects → Dedicated Envoy](https://docs.celum.io/concepts/projects.md#dedicated-envoy).

A fourth, **`kamajiCPAccess`** (Kamaji control planes), defaults to off; creating a Kamaji cluster in the project turns it on.

> [!NOTE]
>
> The isolation policy is **create-once**. Re-applying a project never rewrites an existing one, so edits made in Policy Studio survive. To reset it to the default, toggle isolation off and on again. Changing `observabilityEgress` or `dedicatedGateway` later rewrites it too, and those edits are lost.

## Step 3 — Create it

> [tab] Dashboard
>
> Projects → **Create project**, fill the form, and confirm with **Create project** at its foot.

> [tab] API
>
> `POST /api/supervisors/{name}/projects`. Only `name` is required.

> [tab] MCP
>
> `create_project`.

The name must be a DNS label — `^[a-z0-9][a-z0-9-]{0,40}[a-z0-9]$` — because it becomes a namespace.

Three refusals, each meaning something different:

| Status | Meaning |
| - | - |
| **412** | the supervisor has no pool config. Nothing about your request is wrong |
| **409** | a project of that name exists. Checked up front, so you get this rather than a namespace conflict from deep inside the apply. A 409 whose message starts `allocate LB slice` means something else: the LB band has no free slice for a project sent without `lbCidr` |
| **400** | one of your overrides is outside the supercidr, overlaps something, or was taken between your read and your write |

That last one is worth expecting rather than treating as an error. The taken-set is read live, so two people creating projects at the same moment can collide. Celum's own end-to-end suite treats a 400 here as **"re-read the plan and pick the next free slice"**, and retries twice before giving up. Do the same.

**Result:** `201`, and the project appears in the list.

## Verify

Three checks, in this order. Each one catches something the previous cannot.

**The namespace is `proj-<name>`, not `<name>`.** Read it off the project rather than constructing it — this is the first thing Celum's own test run got wrong on live infrastructure.

**The allocation that came back is the allocation you asked for.** Compare `podCidr`, `lbCidr` and `egressIp` on the created project against what you sent. A mismatch means a re-pick happened somewhere.

**The profile actually took.** `isolationPolicyPresent` is the *actual* state and `defaultIsolationPolicy` is the *desired* one — they are separate fields precisely because they can disagree. With `egressNodeCount` set, check that the expected number of egress IPs exists, one per candidate node.

> [tab] API
>
> `GET /api/supervisors/{name}/projects/{project}` returns all of it, including `egressNodes` with per-node assignments.

> [tab] MCP
>
> `get_project`.

Then check on an **independent path**, the supervisor itself: `kubectl get ciliumpodippools,ciliumloadbalancerippools` lists the project's pools with the ranges Celum returned. The address plan (`GET /api/supervisors/{name}/networking/address-plan`, MCP `get_address_plan`) shows them as taken. A project that looks right in Celum and is absent from the cluster is a real failure, and only a second path can see it.

## If you need to undo it

Deleting a project deletes its namespace and everything in it. It is also the one operation here with no MCP tool: **removing a project requires a person.**

Delete is not always immediate, and the responses say which case you are in:

| Status | Meaning |
| - | - |
| **409** | the project holds guest clusters that require individual deletion approval. The response lists them; approve each one first |
| **202** | CAPI cluster teardown is running. The namespace is deleted when the last cluster is gone, by a background finisher — `clustersRemaining` tells you how many are left |
| **204** | done |

Afterwards, confirm the allocation was released: the project's pools are gone from `kubectl get ciliumpodippools,ciliumloadbalancerippools`, and the address plan offers the range again. A deleted project that leaks its subnet blocks whoever asks for that range next.

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| `412` on a create that looks fine | the supervisor's pool config is not seeded. Check `seeded` on the config read, which returns 200 either way |
| `400` on a CIDR you just saw was free | someone took it between your read and your write. Re-read and re-pick |
| The project exists but has no pod pool | you left `podCidr` empty, which means inherit. A pod pool is created only for a range you name |
| `409` with `allocate LB slice` on a create without `lbCidr` | the LB band is full: every project created without `lbCidr` took a slice of it. Widen the LB band in the address plan |
| Guest agents stop shipping metrics and logs | `observabilityEgress` was turned off while isolation is on |
| Edits to the isolation policy keep surviving a re-apply | by design — it is create-once. Toggle isolation off and on to reset it |
| Everything in the namespace is unreachable | isolation defaults to **on**. That is the intended state, not a fault |
| Delete returns 409 | guest clusters inside need individual approval first |

## Next

> [card] [Projects](https://docs.celum.io/concepts/projects.md)
>
> What a project is, and how its address space is modelled.

> [card] [Project networking](https://docs.celum.io/networking/project-networking.md)
>
> The objects behind the slices, and how egress identity works.

> [card] [Create a VM](https://docs.celum.io/vms/create.md)
>
> The first thing most teams put in a new project.

> [card] [Create a cluster](https://docs.celum.io/clusters/create.md)
>
> Tenant clusters live in a project namespace.


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