# Projects & API keys

> How tenancy works in Lumin. One project owns its data; one API key only writes to its project.

Canonical: https://docs.getlumin.dev/concepts/projects-and-api-keys/

A **project** is the tenancy boundary. Every log row, metric point, browser event, and host snapshot belongs to exactly one project. You can't query across projects from the UI, and you can't ingest into a project you don't own a key for.

An **API key** is a credential scoped to one project. It comes in three **kinds** that decide which endpoints it can hit. No kind can reach the app UI or any other project.

| Kind | Prefix | Endpoints | Use for |
|---|---|---|---|
| **Server / agent** | `lmn_priv_*` | `/v1/logs`, `/v1/metrics`, `/v1/agent/snapshot` | `lumin-agent`, server-side SDKs, anything you run on infrastructure you control. Write-only. |
| **Browser SDK** | `lmn_pub_*` | `/v1/events` | The `@lumin-monitor/browser` SDK baked into a frontend bundle. Write-only. |
| **Coding agent** | `lmn_read_*` | `/v1/mcp` | Claude Code, Cursor, VS Code, and other MCP clients. Read-only; see [the MCP server](https://docs.getlumin.dev/mcp/). |

The server enforces the kind separation in middleware. A `lmn_pub_*` key sent to `/v1/logs` returns 403; a `lmn_priv_*` key sent to `/v1/events` returns 403; a `lmn_read_*` key sent to any ingest endpoint returns 403, and an ingest key sent to `/v1/mcp` returns 403. This is the load-bearing control on browser keys: even though the key is visible to anyone with devtools, it cannot pivot to logs or metrics ingestion. It is also what keeps a read key on a laptop from ever writing, and an agent key on a server from ever reading.

## Where projects come from

You get one project on signup (named "Default"). You can create more under **Settings → Projects → New project**. Common patterns:

- **One project per environment.** `production`, `staging`, `dev` as three separate projects. This is the most common shape — it means a noisy dev host can't drown out production charts.
- **One project per app.** Two projects, `web` and `mobile-backend`, when the teams operating them are different or when you want the dashboards to stay focused.
- **One "infra" project plus one per app.** The agent ships host metrics and infrastructure logs (Caddy, Postgres) into `infra`, while each app has its own project. This keeps "is the VM healthy" separate from "is the app healthy".

You can always merge data by querying both projects in the UI. Splitting later is harder — pick projects on shape boundaries that are unlikely to change.

## How API keys work

Open **Settings → API keys → New key**. Pick a name (e.g. `lumin-agent`), choose a **Kind** (Server / agent, Browser SDK, or Coding agent), and the UI returns the secret *once*. Copy it; Lumin never shows the secret again. The prefix (`lmn_priv_*`, `lmn_pub_*`, or `lmn_read_*`) tells you the kind at a glance — useful when reviewing config diffs or scrolling through a list.

The key is stored on the API as `sha256(secret)`, so a database leak does not expose the secret value. The resolver caches `(hash → project_id)` for 60 seconds in-process so per-request authentication is fast.

### Rotation

The right rotation procedure is **mint new, deploy new, revoke old**:

1. **Settings → API keys** — mint a fresh key on the same project.
2. Update the consumer (the agent's `.env`, your browser `VITE_LUMIN_BROWSER_API_KEY`, …) with the new value and roll the deployment.
3. **Settings → API keys** — revoke the old key.

Revocation invalidates the resolver cache **immediately** — the revoked key stops working on its next request, not after the cache's 60-second TTL expires. This is the property that makes the "mint new → roll → revoke old" sequence safe to do in any order during a key compromise.

### Scoping today

API keys are scoped to a single project. Within a project they are bound to one of three **kinds** (see the table at the top of this page), and the kind boundary is enforced on the server. A leaked browser key can only ingest browser events; a leaked agent key can only ingest server-side telemetry; a leaked coding-agent key can only read. Finer per-endpoint scopes (e.g. "logs but not metrics") are not enforced today.

### What leaks look like

A leaked **browser key** (`lmn_pub_*` — it is necessarily visible in the page bundle) is the most common case. An attacker with the key can:

- Inject fake `page` / `track` / `identify` events into the project.
- Inflate the project's events ingest counters.

They cannot read any data, post logs or metrics, exfiltrate users, reach other projects, or touch the app UI. The kind boundary blocks the pivot to other ingest endpoints. The remediation is rotation, not panic.

A leaked **agent key** (`lmn_priv_*`) is more impactful only in that the agent posts higher-volume data (host snapshots, container metrics, logs). The attacker's capability is still write-only ingest into one project; the agent key cannot post browser events either.

A leaked **coding-agent key** (`lmn_read_*`) is the one kind that exposes data. An attacker with the key can read the project's logs, metrics, checks, alerts, browser events, and host metrics through the MCP server, at the same caps and rate limits a legitimate client gets. They cannot write anything, reach another project, or reach the app UI. Revoke the key; revocation takes effect on the next call. Keep read keys out of tracked files and shared shell histories.

## Who can do what

Lumin has org-level roles (owner, admin, member) and project memberships. Anyone in an org can be added to a project as a member; only owners and admins can mint or revoke API keys. Sessions are cookie-based; CSRF tokens protect every state-changing endpoint.

For the indie-team audience Lumin is built for, this matters less than for an enterprise — most of you are operating as a single owner. The roles exist so the product scales to teams without retrofitting.
