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

# App keys

> Let CI jobs, scripts, and other callers without a browser call one app with scopes the app defines.

An app key lets a caller without a browser session, such as a CI job or a
script, call one app that handles requests. The key works only for that app
and carries only scopes the app declares. Your code decides what each scope
allows.

An app key cannot open another app, act as a person, call the Marina API, or
deploy code. To deploy from CI, use a workspace API key (`mar_…`) with
`MARINA_TOKEN` or `marina setup --token`.

## Declare scopes

Declare scopes in `marina.json` under `access`, then deploy. Each scope maps to
a short description of what it allows. `access` requires an `entrypoint`.

```json marina.json theme={null}
{
  "schema": 1,
  "entrypoint": "src/app.ts",
  "access": {
    "scopes": {
      "builds:read": "Read the previous successful build",
      "builds:publish": "Upload artifacts and publish builds"
    }
  }
}
```

| Rule | Limit |
| - | - |
| Scope name | Lowercase letters and digits, joined by `:`, `.`, `_`, or `-`; starts with a letter; at most 64 characters |
| Scopes per app | 1 to 32 |
| Description | 1 to 200 characters |

## Create and manage keys

A workspace admin or the app's creator manages its keys. Run these commands
in the linked project directory, or pass `--app <slug>`:

```bash theme={null}
marina keys create --name "GitHub Actions" --scope builds:read --scope builds:publish
marina keys
marina keys revoke <key-id>
```

| Command | Result |
| - | - |
| `marina keys create --name <name> --scope <scope> …` | Creates a key and prints it once. |
| `marina keys create … --expires-days <1-365>` | Creates a key that expires after that many days. |
| `marina keys` | Lists active keys with their name, prefix, scopes, and id. |
| `marina keys revoke <key-id>` | Revokes a key. |

Copy the key when it is created. Marina stores only a hash and cannot show it
again; if it is lost, create a new key and revoke the old one. Store the key in
your CI provider's secret store, not in source control.

A key can carry only scopes that the app's live version declares. Key names
are 1 to 60 characters. Keys without `--expires-days` last until revoked.

### With the Marina API

The same operations are available with a workspace API key that has the
`publish` permission. The user who created the API key must be a workspace
admin or the app's creator. `{app}` is the app's slug or id.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/v1/apps/{app}/keys` | Create a key. Body: `name`, `scopes`, optional `expires_in_days`. |
| `GET` | `/v1/apps/{app}/keys` | List active keys. |
| `DELETE` | `/v1/apps/{app}/keys/{id}` | Revoke a key. |

```bash theme={null}
curl --fail-with-body "${MARINA_API}/v1/apps/my-tool/keys" \
  -X POST \
  -H "Authorization: Bearer ${MARINA_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "name": "GitHub Actions", "scopes": ["builds:read"], "expires_in_days": 90 }'
```

The response's `key.token` is the only copy of the key.

## Call the app

Send the key as a bearer token to the app's URL:

```bash theme={null}
curl --fail-with-body "https://my-tool--acme.marina-apps.com/api/builds" \
  -H "Authorization: Bearer ${MARINA_APP_KEY}"
```

Marina removes the `Authorization` header before the request reaches your
app. An `Authorization` value that does not begin with `mak_` is not treated
as an app key; the request follows the normal sign-in flow.

| Request | Response |
| - | - |
| Valid key | Your app's response, unchanged. No cookie is set. |
| Unknown, revoked, or expired key, or a key for another app | `401` with `WWW-Authenticate: Bearer realm="marina"` and a JSON error. Never a sign-in redirect. |
| A path under `/__marina/` or `/.marina/` | `404` |
| Marina cannot check the key | `503`. Retry the request. |

## Check scopes in your app

Each request tells your app who is calling:

| Header | Signed-in user | App key |
| - | - | - |
| `x-platform-principal` | `user` | `app-key` |
| `x-platform-user-id` | The user's id | Not set |
| `x-platform-workspace-id` | The workspace | The workspace |
| `x-platform-key-id` | Not set | The key's id |
| `x-platform-scopes` | Not set | The key's scopes, separated by spaces |

Marina removes any `x-platform-*` header a caller sends, so your app can trust
these values. Check the principal and the scope before doing the work:

```ts theme={null}
function hasScope(request: Request, scope: string): boolean {
  if (request.headers.get("x-platform-principal") !== "app-key") return false;
  const scopes = (request.headers.get("x-platform-scopes") ?? "").split(" ");
  return scopes.includes(scope);
}

if (!hasScope(request, "builds:publish")) {
  return Response.json({ error: "requires builds:publish" }, { status: 403 });
}
```

An app key request has no signed-in user, so code that reads
`x-platform-user-id` must handle its absence. Connection calls act for a
signed-in person, so during an app key request they fail with
`Connection calls require an authenticated app request`.

## Scopes follow the live version

A key presents only the scopes that are both on the key and declared by the
version it reaches. Removing a scope from `marina.json` and deploying, or
rolling back to a version without it, removes that scope from every existing
key. A key with no remaining scopes is refused.

## Revocation and expiry

Revoking a key, its expiry, and scope changes take effect within 30 seconds.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.