Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 49 additions & 29 deletions content/access/audit-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,96 @@
title: 'Audit log'
linkTitle: 'Audit log'
weight: 5
description: 'Who did what, when. Filterable by resource, outcome, and time range.'
lead: 'Every state-changing API call lands in the audit log — actor, action, resource, outcome, and timestamp. Use it to answer "who deployed that?" and "what was tried but blocked?"'
description: 'Who did what, when. Filterable by resource, actor, outcome, and time range.'
lead: 'Every state-changing API call that passes authorization lands in the audit log — actor, action, resource, outcome, and timestamp. Use it to answer "who deployed that?" and "which writes failed?"'
---

## What gets logged

Every call that creates, updates, or deletes a resource — `deployment.deploy`,
`deployment.rollback`, `domain.create`, `route.delete`, `role.bind`,
`serviceAccount.createKey`, and so on — is recorded. Read-only calls
(`*.list`, `*.get`, `*.metrics`) are not.
Every call that creates, updates, or deletes a resource — a deploy, a rollback,
a domain create, a route delete, a role grant, a service-account key create, and
so on — is recorded once it passes authorization. Read-only calls (`*.list`,
`*.get`, `*.metrics`) are not.

{{< callout type="note" >}}
**Permission denials are not logged.** A call rejected by the permission check
returns before the write is attempted, so it never reaches the audit log. The
log records *authorized* writes — the ones that ran — not access attempts. To
reason about who *can* do what, use [roles](/access/roles/), not the audit log.
{{< /callout >}}

Each entry captures:

- **Actor** — the principal who made the call (user email or service-account
email), plus their type.
- **Action** — the API function name, e.g. `deployment.deploy`.
- **Resource** — the type, ID, name, and location of the affected resource.
- **Outcome** — `success`, `forbidden`, or `error`.
- **Detail** — a short human-readable summary (e.g. *"Deployed revision 7"*).
email), plus their type (`User` or `ServiceAccount`).
- **Action** — the verb that ran: `deploy`, `create`, `delete`, `rollback`,
`grant`, `revoke`, and so on.
- **Resource** — the `type` (`deployment`, `domain`, `role`, …, lowercase), plus
the id, name, and location of the affected resource.
- **Outcome** — `success` or `failure`. A `failure` is an authorized call that
*ran but errored* — a validation error, a conflict, a downstream failure — not
a permission denial.
- **Detail** — a short human-readable summary (e.g. *"revision 7"*).
- **Created at** — when the call happened, in UTC.

## Filter and browse

The Audit Logs page lets you narrow by:

- **Resource type** — Deployment, Domain, Route, Disk, Role, ServiceAccount, …
- **Outcome** — success, forbidden, error.
- **Outcome** — success or failure.
- **Date range** — today, last 7 days, last 30 days, last 90 days, last year,
or a custom range.

The same filters are available on the API:
The `auditLog.list` function takes the same filters — plus an `actor` filter the
console doesn't expose — and a `limit`. `resourceType` and `outcome` match
exactly, and the resource type is **lowercase** (`deployment`, not
`Deployment`). The time window is `after` / `before` (RFC 3339). There is no
`action` filter — narrow by `resourceType`, then read the `action` field on each
entry.

```bash
curl https://api.deploys.app/auditLog.list \
-H "Authorization: Bearer $DEPLOYS_TOKEN" \
-d '{
"project": "acme",
"resourceType": "Deployment",
"resourceType": "deployment",
"outcome": "success",
"from": "2026-05-01T00:00:00Z",
"to": "2026-06-01T00:00:00Z"
"after": "2026-05-01T00:00:00Z",
"before": "2026-06-01T00:00:00Z",
"limit": 200
}'
```

The response is a paginated list of entries you can sift through or pipe
through `jq` for ad-hoc analysis.
The response is a list of entries you can sift through or pipe through `jq` for
ad-hoc analysis.

## Common queries

- **"Who deployed the bad release?"** — filter by `Deployment` resource type,
action `deployment.deploy`, and the rough time window. The actor on the
- **"Who deployed the bad release?"** — filter by `deployment` resource type and
the rough time window, then look for `action: "deploy"`. The actor on the
matching entry is your culprit (or your hero).
- **"What was blocked?"** — outcome `forbidden`. Useful when adjusting
[roles](/access/roles/) — a string of failures usually means someone needs a
permission you forgot to add.
- **"What did the CI service account touch yesterday?"** — set the actor email
on the API call (the console doesn't expose this filter directly).
- **"Which writes failed?"** — outcome `failure`. These are calls that were
*allowed* but errored while running — a bad request body, a name conflict, a
downstream failure — so it's a debugging aid for automation, not a record of
blocked access.
- **"What did the CI service account touch yesterday?"** — set `actor` to the
service-account email plus an `after` / `before` window (the console doesn't
expose the actor filter directly).

## Retention

Entries are retained for the lifetime of the project. Deleting a resource
doesn't delete its audit history; deleting the *project* eventually purges
it, but a record of project deletion itself is retained.
Entries are kept for **one year** after they're written, then purged
automatically by the database's row-level TTL — no action needed on your part.
Deleting a resource doesn't remove its existing audit history; the entries age
out on the same one-year clock.

## Streaming to your SIEM

Two patterns work well:

- **Pull periodically.** A small service account with `auditLog.list` polls
every few minutes for new entries (filter by `from` ≥ last-seen
every few minutes for new entries (filter by `after` ≥ last-seen
`createdAt`) and forwards to your aggregator.
- **Pull at quarter-of-the-hour cadence** if you only need rough
near-real-time. Run a cron deployment with `--type CronJob` and
Expand Down
15 changes: 15 additions & 0 deletions content/access/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ Returns the project's current monthly usage rolled up by resource type. The
same data drives the project dashboard and feeds the
[billing report](/billing/usage-reports/).

For charts rather than a single rollup, two API functions return time-series
over a `timeRange` (`7d`, `30d`, or `90d`):

- **`project.metrics`** — CPU, memory, disk, egress, replica, and static-storage
usage over time, the series behind the project dashboard.
- **`project.storageMetrics`** — static-site storage held over time.

```bash
curl https://api.deploys.app/project.metrics \
-H "Authorization: Bearer $DEPLOYS_TOKEN" \
-d '{ "project": "acme", "timeRange": "30d" }'
```

Both need only `project.get`.

## Deleting a project

A project can be deleted only when it's empty — every deployment, domain,
Expand Down
61 changes: 41 additions & 20 deletions content/access/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,47 @@ deploys role create \
--permissions "project.get,deployment.list,deployment.get,deployment.deploy,registry.list"
```

The `--permissions` flag takes a comma-separated list of API function names.
Each function name (e.g. `deployment.deploy`) is one permission — the same
string you'd POST to as `https://api.deploys.app/<permission>`.

The most useful permissions:
The `--permissions` flag takes a comma-separated list of permission strings.
A permission is `<namespace>.<action>`, and unlike the API function names it is
**always lowercase** — the `serviceAccount.createKey` call, for example, is
guarded by the `serviceaccount.key.create` permission. Matching is exact, so the
casing matters: `serviceAccount.createKey` in a role grants nothing. The
authoritative list is whatever `role.permissions` returns; the most useful ones:

| Group | Permissions |
|---|---|
| Project | `project.get`, `project.update`, `project.delete`, `project.usage` |
| Deployment | `deployment.list`, `deployment.get`, `deployment.deploy`, `deployment.delete`, `deployment.pause`, `deployment.resume`, `deployment.rollback`, `deployment.metrics`, `deployment.revisions` |
| Domain | `domain.list`, `domain.get`, `domain.create`, `domain.delete`, `domain.purgeCache` |
| Route | `route.list`, `route.create`, `route.delete` |
| Project | `project.get`, `project.delete` |
| Deployment | `deployment.list`, `deployment.get`, `deployment.deploy`, `deployment.delete` |
| Domain | `domain.list`, `domain.get`, `domain.create`, `domain.delete`, `domain.purgecache` |
| Route | `route.list`, `route.get`, `route.create`, `route.delete` |
| Firewall (WAF) | `waf.list`, `waf.get`, `waf.set`, `waf.delete` |
| Cache | `cache.list`, `cache.get`, `cache.set`, `cache.delete` |
| Disk | `disk.list`, `disk.get`, `disk.create`, `disk.update`, `disk.delete` |
| Registry | `registry.list`, `registry.delete`, `registry.deleteManifest`, `registry.untag` |
| Pull secret | `pullSecret.list`, `pullSecret.create`, `pullSecret.delete` |
| Role | `role.list`, `role.create`, `role.delete`, `role.bind` |
| Service account | `serviceAccount.list`, `serviceAccount.create`, `serviceAccount.delete`, `serviceAccount.createKey` |
| Billing | `billing.get`, `billing.report`, `billing.listInvoices` |
| Audit | `auditLog.list` |

The wildcard `*` matches everything.
| Registry | `registry.list`, `registry.get`, `registry.pull`, `registry.push` |
| Pull secret | `pullsecret.list`, `pullsecret.get`, `pullsecret.create`, `pullsecret.delete` |
| Env group | `envgroup.list`, `envgroup.get`, `envgroup.create`, `envgroup.update`, `envgroup.delete` |
| Workload identity | `workloadidentity.list`, `workloadidentity.get`, `workloadidentity.create`, `workloadidentity.delete` |
| Service account | `serviceaccount.list`, `serviceaccount.get`, `serviceaccount.create`, `serviceaccount.delete`, `serviceaccount.key.create`, `serviceaccount.key.delete` |
| Role | `role.list`, `role.get`, `role.create`, `role.delete`, `role.bind` |
| GitHub | `github.list`, `github.link`, `github.unlink`, `github.update` |
| Email | `email.list`, `email.send` |
| Dropbox | `dropbox.list`, `dropbox.upload` |
| Static sites | `site.publish` |
| Audit | `auditlog.list` |

Some actions don't have their own permission — they ride on a broader one.
`deployment.metrics`, `deployment.revisions`, `deployment.rollback`,
`deployment.pause`, and `deployment.resume` are all covered by `deployment.get`
or `deployment.deploy`; `registry.delete` / `deleteManifest` / `untag` are
covered by `registry.push`; `project.usage` and `project.metrics` by
`project.get`. Granting the made-up string does nothing — grant the broader
permission instead.

The wildcard `*` matches everything, and each namespace has a `<namespace>.*`
(for example `domain.*`) covering that namespace's actions.

Billing access isn't a project permission: billing calls are authorized by
**ownership of the billing account**, not by a role in the project.

## Binding roles

Expand Down Expand Up @@ -104,10 +124,11 @@ The audit log records permission denials too — failed calls show up with

- **Deployer for CI**, restricted to `deployment.deploy` and the read calls
needed to inspect status. CI never needs to manage roles or delete projects.
- **Viewer for the on-call rotation**, plus `deployment.metrics` and
`auditLog.list` if you want them to debug without changing anything.
- **Viewer for the on-call rotation**, plus `auditlog.list` if you want them to
debug without changing anything. (Viewer already has `deployment.get`, which
covers metrics and revisions.)
- **Operator** with everything except `project.delete`, `role.bind`, and
`serviceAccount.createKey`. Day-to-day powerful, but can't change who has
`serviceaccount.key.create`. Day-to-day powerful, but can't change who has
access or generate new machine credentials.

Keep `admin` to the smallest practical group; everyone else gets a tailored
Expand Down
25 changes: 21 additions & 4 deletions content/api/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ The big picture. Each row is a fully-qualified API function.
| `domain.list` / `.get` / `.create` / `.delete` | Domain CRUD |
| `domain.purgeCache` | CDN cache purge for a domain |
| `route.list` / `.create` / `.createV2` / `.delete` | Route CRUD |
| `waf.list` / `.get` / `.set` / `.delete` | Firewall zone CRUD |
| `waf.metrics` | Firewall match counts over time |
| `waf.list` / `.get` / `.set` / `.delete` | Firewall zone CRUD (rules + rate limits) |
| `waf.metrics` / `.limitMetrics` | Firewall match counts and rate-limit decisions over time |
| `cache.list` / `.get` / `.set` / `.delete` | Cache-override zone CRUD |
| `cache.metrics` | Cache-override decision counts over time |

### Storage and registry

Expand Down Expand Up @@ -132,12 +134,27 @@ The big picture. Each row is a fully-qualified API function.
| `billing.listInvoices` / `.getInvoice` / `.downloadInvoice` | Invoices |
| `billing.uploadTransferSlip` | Submit a bank-transfer receipt |

### GitHub

For [build-and-deploy from GitHub](/automation/deploy-from-github/). These manage
the link between a repository and the service account it deploys as.

| Function | What it does |
|---|---|
| `github.list` | Repositories linked in the project |
| `github.link` / `.unlink` | Link or unlink a repository to a service account |
| `github.update` | Change a link's service account, deploy trigger, or production branch |

The keyless token exchange and build/deploy status reporting (`github.exchangeToken`,
`github.notify`) are authenticated by the workflow's GitHub OIDC token and called
by the [build-and-deploy action](/automation/deploy-from-github/), not directly.

### Other

| Function | What it does |
|---|---|
| `email.list` | Email domains attached to the project |
| `dropbox.list` / `.metrics` | Dropbox uploads (alpha) |
| `email.list` / `.send` | List project email domains; send a transactional email |
| `dropbox.list` / `.metrics` | [Dropbox](/storage/dropbox/) stored files and usage |

## Typed clients

Expand Down
27 changes: 24 additions & 3 deletions content/deployments/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,30 @@ Optional `subPath` mounts a sub-directory of the disk only.

## Sidecars — `sidecars`

Each entry has its own `name`, `image`, `port`, `resources`, `command`, `args`,
and `env`. Use them sparingly — they share the pod with the main container and
its lifecycle.
A sidecar is a helper container that shares the pod with your main container and
its lifecycle. Today the only supported sidecar is the **Cloud SQL Auth Proxy**,
configured under `cloudSqlProxy` — you don't supply an arbitrary image:

```json
"sidecars": [
{
"cloudSqlProxy": {
"instance": "my-project:asia-southeast1:main",
"port": 5432,
"credentials": "<service-account-json>"
}
}
]
```

- **`instance`** — required; the Cloud SQL instance connection name.
- **`port`** — the local port the proxy listens on (default `3300`). Your app
connects to the database at `127.0.0.1:<port>`.
- **`credentials`** — optional service-account JSON for the proxy; omit it to use
the deployment's ambient credentials.

The platform runs this as a `cloudsql-proxy` container alongside yours. Arbitrary
sidecar containers (your own image, command, and env) aren't supported yet.

## TTL and one-shot jobs — `ttl`

Expand Down
5 changes: 5 additions & 0 deletions content/networking/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ A `cache` override is described by a few fields:
errors.
- **`status`** — optional; force only these origin response statuses (e.g.
`[200, 301]`). Empty forces every cacheable status.
- **`priority`** — orders the `cache` rules against each other; the lowest number
wins and declaration order breaks ties. `0` resolves to the default (`100`).
`bypass` rules aren't ordered — a matching bypass always wins.
- **`mode`** — `enforce` (default) applies the override; `shadow` evaluates and
counts it without changing caching. See [Roll out safely](#roll-out-safely).

## The filter expression

Expand Down
Loading