Projects
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.
CodeSpar uses a 2-level tenancy model: Account -> Project. Every resource -- 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).
- 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:
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). When you need the fact, read key.environment from whoami 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. |
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.
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) 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/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 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:
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:
- If the API key is project-scoped (pinned to one project), the key's project always wins and
x-codespar-projectis ignored. - Otherwise, if
x-codespar-projectis present, that project is used. - Otherwise, the request falls back to the account's default project.
The request will fail with 404 not_found if the project ID does not belong to the authenticated account. Projects never cross account boundaries.
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-projectand 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.
Deleting a project deletes its API keys, triggers, sessions history, and connection records. There is no undo.
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
Why meta-tools
CodeSpar provides 14 meta-tools that abstract every connected MCP server into a unified commerce interface, reducing context window cost and simplifying agent development.
Connect Links
Hosted OAuth flow that lets your end users connect their own Stripe, Mercado Pago, Shopify, and other provider accounts, without you building or maintaining a connection UI.