---
title: Projects
description: Projects are the second level of CodeSpar's 2-level tenancy model -- an isolation boundary inside an account for API keys, connections, triggers, sessions, and events.
---

import { Callout } from "fumadocs-ui/components/callout";

CodeSpar uses a **2-level tenancy model**: **Account -> Project**. Every resource -- [sessions](/docs/concepts/sessions), API keys, triggers, connections, events -- belongs to exactly one project, and every project belongs to exactly one account.

Projects are the unit of environment isolation. A typical setup has one account per company and multiple projects inside it (for example `dev`, `staging`, `prod`), each with its own API keys, connected accounts, and audit log.

## Why projects

Before projects, every resource was scoped to the account. That forced teams to either share credentials across environments or spin up separate accounts just to keep `dev` from touching `prod`. Projects fix that without splitting billing, team membership, or settings:

- **Environment isolation** -- dev, staging, and prod live side by side under one account with independent API keys and OAuth connections.
- **Per-project audit** -- tool calls, sessions, and events are filtered by project in the dashboard.
- **Per-project limits** -- quota and rate-limit counters can be scoped per project (see [Billing](/docs/concepts/billing)).
- **Same team, same billing** -- members, plan, and invoice live at the account level and apply across all projects.

## The default project

Every account has a **default project**, auto-created at signup. If you never touch projects, everything you create lands there and the HTTP API works exactly as before -- no header required.

| Property | Value |
|----------|-------|
| Auto-created | On account signup |
| `is_default` | `true` |
| Slug | `default` (reserved) |
| Deletable | No -- protected by `cannot_delete_default` |

Find your default project's ID in the dashboard under **Dashboard -> Projects**, or via the API:

```bash
curl "https://api.codespar.dev/v1/projects?is_default=true" \
  -H "Authorization: Bearer csk_live_..."
```

## Project IDs and slugs

| Field | Format | Notes |
|-------|--------|-------|
| `id` | `prj_<16chars>` | Stable identifier. Used in headers, URLs, webhooks. |
| `slug` | lowercase alphanumeric + `_`/`-`, max 64 chars | Unique per account. `default` is reserved. Used in dashboard URLs. |
| `name` | free-form string | Display name only. |
| `environment` | `live` or `test` | Set on creation. **Immutable.** Determines which provider URLs the project's connections use (sandbox vs production) and the environment of every API key minted in the project. |

## Environments — live vs test

Every project has an `environment` field, set at creation and **immutable** afterward. It is the only place the environment lives. A key's environment is its project's: the `csk_live_` or `csk_test_` prefix is stamped from the project's `environment` when the key is minted and is never read again. On every request the server resolves the key's project and takes `environment` from that row.

| Field | What it controls |
|-------|------------------|
| `project.environment` | Which provider URL the request lands on. A connection in a `test` project hits `sandbox.melhorenvio.com.br`; the same provider in a `live` project hits `melhorenvio.com.br`. Same OAuth token storage shape, different upstream host. |
| API key prefix | Nothing on a request. The prefix is a projection of the project's `environment`, stamped at mint time, so it tells you which environment the key acts in. The server never compares it to the project. No status code exists for a prefix that disagrees with its project, because the mint never issues one (see [Minting a key](#minting-a-key)). When you need the fact, read `key.environment` from [`whoami`](/docs/cookbooks/projects-and-keys) rather than the string. |
| Dashboard env toggle | A pill in the dashboard header (`Live` / `Test`) filters which projects show up in the Project Switcher and which environment the "Create Project" modal defaults to. Stored in the `codespar_env` cookie. Cosmetic — does not change auth. |

<Callout type="warn">
**Why `environment` is immutable:** the field is enforced via a Postgres trigger (`projects_environment_immutable`). A project that started in `test` cannot be promoted to `live` — the connections, vault entries, and webhook history were authored against sandbox URLs and would be wrong against production. The intended path is: create a separate `live` project, run OAuth fresh against the production provider hosts, and switch your application config to point at the new project.
</Callout>

### Minting a key

A key is minted in a project, and its environment is that project's. The mint (`POST /v1/api-keys`, a dashboard operation; see [Creating keys in the dashboard](/docs/concepts/authentication#creating-keys-in-the-dashboard)) derives the key's environment from the project the request resolves to. The request body may name an `env`, and when it differs from the project's environment the mint is refused before any key exists:

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "env_mismatch",
  "message": "this project is live; an api key issued here is a live key. Create the key in a test project instead."
}
```

The dashboard's mint modal at [Dashboard → API Keys](/dashboard/api-keys) follows the same rule. Its Environment field defaults to the active project's environment, so a key minted in a `test` project is `csk_test_*` and a key minted in a `live` project is `csk_live_*`; picking the other environment there gets the same `env_mismatch` from the mint, and no key is created. To mint a key for the other environment, switch to a project of that environment first. New accounts get a `test`-environment project plus a `csk_test_*` key auto-created at signup, so the common first-day path is "click the auto-minted key, start building".

## Passing a project on requests

All `/v1` endpoints accept the `x-codespar-project` header:

```bash
curl -X POST https://api.codespar.dev/v1/sessions \
  -H "Authorization: Bearer csk_live_..." \
  -H "x-codespar-project: prj_a1b2c3d4e5f6g7h8" \
  -H "Content-Type: application/json" \
  -d '{"servers": ["stripe"]}'
```

**Resolution order:**

1. If the API key is **project-scoped** (pinned to one project), the key's project always wins and `x-codespar-project` is ignored.
2. Otherwise, if `x-codespar-project` is present, that project is used.
3. Otherwise, the request falls back to the account's **default project**.

<Callout type="info">
The request will **fail** with `404 not_found` if the project ID does not belong to the authenticated account. Projects never cross account boundaries.
</Callout>

## Project-scoped API keys

An API key can optionally be pinned to a single project via its `project_id` column. When a request authenticates with a project-scoped key:

- The key's project is resolved automatically -- you do **not** need to send `x-codespar-project`.
- If the key holder does send `x-codespar-project` and it does not match the key's project, the header is ignored (the key wins).

This is the recommended pattern for CI/CD, production workloads, and anything else that should be locked to one environment. Create project-scoped keys from **Dashboard -> Projects -> [project] -> API Keys**.

Unpinned keys (no `project_id`) behave like account-level keys: they can operate on any project via `x-codespar-project`, and fall back to the default project when the header is omitted.

## Dashboard routing

Per-project surfaces in the dashboard live under `/dashboard/projects/[projectId]/...`:

| Surface | Route |
|---------|-------|
| API Keys | `/dashboard/projects/[projectId]/api-keys` |
| Triggers | `/dashboard/projects/[projectId]/triggers` |
| Sessions | `/dashboard/projects/[projectId]/sessions` |
| Sandbox | `/dashboard/projects/[projectId]/sandbox` |
| Servers | `/dashboard/projects/[projectId]/servers` |

A **Project Switcher** in the sidebar swaps the active project across all of these. Project CRUD lives at `/dashboard/projects`.

## Common workflows

### Managing projects is an admin's, on the dashboard

Creating, renaming, promoting, archiving and deleting a project, and writing its settings, are administrative. An API key gets `403 admin_session_required` for them, whatever its scopes and even on the project it was issued in; an admin or owner does them from the dashboard. A key reads the organization's projects and its own project's settings, and never acts on another project (`403 api_key_other_project`).

### Promote a non-default project to default

`is_default` is promoted atomically via `PATCH /v1/projects/:id` with `{"is_default": true}`. The previous default is demoted in the same transaction. You cannot **un-set** `is_default` directly -- to change the default, promote a different project instead.

### Delete a project

From the dashboard (an API key gets `403 admin_session_required`).

Deletion fails with:

- `cannot_delete_default` -- promote a different project to default first.
- `cannot_delete_last_project` -- every account must have at least one project.

<Callout type="warn">
Deleting a project deletes its API keys, triggers, sessions history, and connection records. There is no undo.
</Callout>

### Roles per project

Users inherit their account's role in every project. Managing roles for a specific project is not available yet, in the dashboard or the public API.

## Next steps

<NextStepsGrid items={[
  { label: "REFERENCE", title: "Projects API", description: "Full HTTP reference for /v1/projects — create, list, update.", href: "/docs/api/projects" },
  { label: "CONCEPT", title: "Sessions", description: "How sessions inherit the request's resolved project.", href: "/docs/concepts/sessions" },
  { label: "CONCEPT", title: "Authentication", description: "API key types — including project-pinned keys.", href: "/docs/concepts/authentication" },
  { label: "COOKBOOK", title: "Multi-Tenant Agent", description: "Using projects + metadata.tenant_id for isolation.", href: "/docs/cookbooks/multi-tenant" },
]} />
