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

# Project configuration

> Every field of a project's configuration, the destinations it can file to, and the limits that apply.

A project is the unit a widget embed points at. It decides where reports are filed, which sites may
submit to it, and who can see the launcher. You normally edit this through the admin panel rather
than by hand, and the panel writes the shape below, with the one exception called out in the table.

## Fields

| Field             | Type         | Notes                                                                                                                                                |
| ----------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `origins`         | `string[]`   | Required. Sites allowed to submit. Each must be a full `scheme://host[:port]`.                                                                       |
| `destination`     | object       | Where issues are filed. See below.                                                                                                                   |
| `teamId`          | `string`     | Legacy Linear team. Required only when there is no `destination`.                                                                                    |
| `linearProjectId` | `string`     | Legacy Linear project, used with `teamId`.                                                                                                           |
| `labelIds`        | `string[]`   | Legacy Linear labels, used with `teamId`. The one field the admin panel does not write: it is read where present, but only `PROJECT_MAP` can set it. |
| `name`            | `string`     | Display name in the admin panel.                                                                                                                     |
| `mode`            | `string`     | Default visibility: `on`, `query`, `manual` or `key`.                                                                                                |
| `help`            | `string`     | URL behind the widget's "Need help?" control.                                                                                                        |
| `accessKey`       | `string`     | The secret `key` mode checks.                                                                                                                        |
| `keyParam`        | `string`     | Which query parameter carries it.                                                                                                                    |
| `tags`            | `TagDef[]`   | Selectable tags, each `{ id, name, color? }`.                                                                                                        |
| `theme`           | `{ accent }` | Pro only. A per-project accent colour.                                                                                                               |
| `updatedAt`       | `number`     | Set by the relay when the project is saved.                                                                                                          |

### Validation

The relay rejects a project that breaks any of these, rather than storing it and failing later:

| Rule                 | Limit                                                                        |
| -------------------- | ---------------------------------------------------------------------------- |
| Origins per project  | 50                                                                           |
| Length of one origin | 253 characters                                                               |
| Access key           | at least 16 characters                                                       |
| Key parameter        | 1 to 40 characters, `a-z A-Z 0-9 _ -`                                        |
| Theme accent         | six-digit hex, such as `#0a2540`                                             |
| Mode                 | must be one of the four; unlike the embed, an invalid value here is an error |

Origins are scanned on every public request, so the cap on their number and length is a
throughput concern as much as a correctness one.

## Destinations

`destination` is one of three shapes, and its `kind` decides which fields apply.

```json theme={null}
{ "kind": "linear", "teamId": "...", "projectId": "...", "labelIds": ["..."] }
```

```json theme={null}
{ "kind": "github", "owner": "...", "repo": "...", "labels": ["..."], "assignees": ["..."] }
```

```json theme={null}
{ "kind": "gitlab", "host": "...", "projectId": "...", "labels": ["..."], "assignees": ["..."] }
```

Only `owner` and `repo` are required for GitHub, and only `projectId` for GitLab. `host` lets a
GitLab destination point at a self-managed instance instead of gitlab.com.

A project saved before destinations existed has no `destination` at all. Those still work: the
relay reads the flat `teamId`, `linearProjectId` and `labelIds` fields as an implicit Linear
destination, so nothing had to be migrated. If you set both, `destination` wins, except that a
Linear destination with no `labelIds` of its own falls back to the flat `labelIds`.

### Tokens

The API token is held by the relay and never reaches the browser, which is the reason the relay
exists at all.

You can set one token for the whole relay, and optionally override it on a single destination. A
per-destination token wins where it is set, so one relay can file into two repositories owned by
different people without either token being able to reach the other's.

What the token has to be able to do, which is all the relay ever asks of it:

| Destination | The relay calls                              | So the token needs                                            |
| ----------- | -------------------------------------------- | ------------------------------------------------------------- |
| Linear      | the GraphQL API, plus a file upload          | permission to create issues and upload files in the workspace |
| GitHub      | `POST /repos/{owner}/{repo}/issues`          | permission to create issues on that one repository            |
| GitLab      | `POST /projects/{id}/uploads` then `/issues` | API access to that one project                                |

Nothing else is called, so a token scoped tightly to one repository or project is enough. Grant no
more than that.

### Where the screenshot ends up, which differs by destination

This is worth knowing before you pick one, because it decides what retention means for you.

**Linear and GitLab** receive the image itself: the relay uploads it to the tracker, and it lives
there for as long as the issue does. Relay retention does not touch it.

**GitHub** has no attachment API the relay can use this way, so the relay stores the screenshot
itself and puts a signed link to it in the issue body. The image is served from your own relay,
which means retention applies to it: once the window passes, the issue keeps its text and its link
stops resolving.

If you self-host and file to GitHub, that is an argument for the unlimited retention a Pro licence
gives you, or for keeping the screenshots yourself.

## Content Security Policy

If your site sends a `Content-Security-Policy` header, the widget needs two directives to work and
a third to take screenshots. Nothing else.

| Directive     | Value               | Needed for               |
| ------------- | ------------------- | ------------------------ |
| `script-src`  | your relay's origin | Loading the embed script |
| `connect-src` | your relay's origin | Sending the report       |
| `img-src`     | `data:`             | Taking the screenshot    |

The relay's origin is the host in your embed `src`: `https://feedback.example.com` for a self-host,
`https://relay.burnsidesteps.com` for the hosted service.

**`img-src data:` is not optional if you want screenshots.** The capture renders your page into a
data URI and loads it back as an image, so a policy without it fails the capture outright rather
than degrading: the report still sends, with no screenshot attached. The browser console says so at
the moment it happens, naming this directive.

**You do not need `style-src 'unsafe-inline'`.** The widget's own CSS is applied as a constructed
stylesheet, which is not an inline style as far as the policy is concerned, so a strict `style-src`
leaves the widget looking exactly as it should. On a browser old enough to lack constructed
stylesheets the widget falls back to an inline `<style>`, which a strict `style-src` will block; it
still works, it just renders unstyled.

You do not need `font-src` either. The capture is told not to fetch remote fonts.

A policy that covers all three looks like this:

```
Content-Security-Policy:
  script-src 'self' https://feedback.example.com;
  connect-src 'self' https://feedback.example.com;
  img-src 'self' data:;
```

If you use `default-src` rather than naming each directive, the same values apply to it.

## Plan limits

|                      | Free    | Pro                          |
| -------------------- | ------- | ---------------------------- |
| Projects             | 3       | Unlimited                    |
| Screenshot retention | 90 days | Until you delete the account |
| Widget badge         | Shown   | Removable                    |
| Per-project theming  | No      | Yes                          |

**Only the hosted service enforces this.** On a relay you run yourself there is no sweep at all:
the Cloudflare Worker template ships no cron, and the Node self-host keeps them until you remove
them unless you set `SHOT_RETENTION_DAYS`, which defaults to keeping them indefinitely. The figure
above describes what hosted applies to a Free account, not a ceiling your own relay imposes on you.

On hosted, a nightly cron applies the window, so a screenshot outlives it by about a day. That
sweep resumes across runs rather than restarting, so as the number of accounts grows a given
account is reached every few nights rather than every night.

Retention covers screenshots the relay is storing, which in practice means GitHub destinations: see
above. The issue itself is never touched. It lives in your tracker, and nothing here can remove it.

## Rate limits

These are per relay, and identical across the Cloudflare Worker and the Node self-host.

| Limit                      | Rate          |
| -------------------------- | ------------- |
| Submissions per project    | 10 per minute |
| Submissions per visitor IP | 5 per minute  |
| Access key checks          | 30 per minute |
| Admin login attempts       | 5 per minute  |

The per-project and per-IP submission limits work together. The per-project ceiling protects your
tracker from a flood, and the per-IP limit stops one visitor consuming that whole allowance and
locking everyone else out.
