---
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";

# Projects

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). |

## Environments — live vs test

Every project has an `environment` field, set at creation and **immutable** afterward. This is independent of the API key prefix (`csk_live_` vs `csk_test_`), but the two must match — a `csk_test_` key cannot operate on a `live` project, and vice-versa.

| 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 | What environment the key is allowed to access. `csk_test_*` only works against `test` projects; `csk_live_*` only against `live`. Mismatch returns `401 unauthorized`. |
| 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>

### Matched-environment mint default

The dashboard's mint modal at [Dashboard → API Keys](/dashboard/api-keys) defaults each new key's prefix to match the active project's environment — a key minted against a `test` project is always `csk_test_*`; a key minted against a `live` project is always `csk_live_*`. 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" — no environment-mismatch step.

For the cross-project case (minting a `csk_live_*` key against a `live` project from a session whose default project is `test`, or vice versa), the explicit environment toggle on the mint modal is still available. The default just stops being wrong for the common case — the pre-default-matched ticket where the operator minted a `csk_live_*` key against a `test` default project and saw `401 unauthorized` on every request no longer fires.

## 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

### 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

```bash
curl -X DELETE https://api.codespar.dev/v1/projects/prj_... \
  -H "Authorization: Bearer csk_live_..."
```

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>

### Grant per-project access without making someone an account admin

By default, every account member inherits the same role across **all** projects. To narrow or widen that — give a contractor admin access to one staging project without exposing production, or give a member admin rights on a specific project — use **project-level role overrides**:

```bash
curl -X POST https://api.codespar.dev/v1/projects/prj_.../members \
  -H "Authorization: Bearer csk_live_..." \
  -d '{"user_id": "user_contractor456", "role": "admin"}'
```

Absence of an override = inherits the account role. Endpoints documented in [Projects API → Project members](/docs/api/projects#project-members-rbac-overrides). Dashboard UI is on the roadmap; for now, this is API-only.

## 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" },
]} />
