# Incidents

> Incidents with a severity, a status timeline, affected components and postmortems, and a public status page that anyone can read through one function that returns only public fields.

Source: https://bettersupabase.com/docs/blocks/incidents

`incidents` is for running an outage and telling customers about it. You open
an incident with a severity and the components it affects, post timeline
updates as it moves from investigating to resolved, and write a postmortem
afterwards. Each organization can publish one status page: the public
components with their status and daily uptime, and the public incident
updates. Visitors read it without signing in.

```bash
better-supabase add incidents
```

```ts
import { createIncidents } from "better-supabase/blocks/incidents";
```

The module requires `tenant` and `access`. With
[notifications](/docs/blocks/notifications) the commander is notified when an
incident opens, with `outbox` it records the events, and with `audit` every
change is audited. The choices behind the anonymous surface are in
[ADR 0084](https://github.com/ScaleDockHQ/better-supabase/blob/main/docs/decisions/0084-anonymous-status-page.md).

## Tables [#tables]

| Table                      | Holds                                                                                                    |
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
| `status_components`        | The services a status page reports on: a name, a group, whether it is `public`, and its current `status` |
| `status_component_history` | One row per status a component held, with its start and end, for the uptime history                      |
| `incidents`                | The title, description, severity, status, whether it is public, the commander, and the start and end     |
| `incident_updates`         | The timeline: each update's status, text and whether it is public                                        |
| `incident_impacts`         | The components an incident affects and how                                                               |
| `incident_postmortems`     | One postmortem per incident: summary, impact, root cause, resolution and action items                    |
| `status_pages`             | One page per organization, with a slug unique across organizations                                       |

Members with `incidents.read` read every table. `anon` has no privileges on
any of them.

## Components [#components]

```ts
const incidents = createIncidents({ transport });

const api = await incidents
  .saveComponent(organizationId, { name: "API", group: "Platform" })
  .orThrow();
await incidents
  .saveComponent(organizationId, { name: "Primary database", public: false })
  .orThrow();
```

A component is public unless `public` is `false`. Its status is never set by
hand: it is the worst impact of the open incidents on it (`maintenance`,
`degraded`, `partial_outage`, `major_outage`), and `operational` when none is
open. Every change is kept in the history the uptime is read from. Components
and the status page need `incidents.manage`.

## Run an incident [#run-an-incident]

```ts
const incident = await incidents
  .open(organizationId, {
    title: "Elevated API errors",
    severity: "sev2",
    public: true,
    commanderId,
    message: "We are looking into elevated error rates.",
    impacts: [{ componentId: api.id, impact: "partial_outage" }],
  })
  .orThrow();

await incidents
  .postUpdate(incident.id, {
    status: "identified",
    body: "A replica fell behind; failing over.",
    public: false,
  })
  .orThrow();
await incidents
  .postUpdate(incident.id, { status: "resolved", body: "Resolved." })
  .orThrow();
```

An incident starts `investigating` with severity `sev3` unless you pick one,
and is private unless `public` is `true`. `message` becomes the first
timeline update. Each `postUpdate` adds to the timeline and moves the
incident to the update's status (`investigating`, `identified`, `monitoring`
or `resolved`); resolving sets `resolvedAt` and returns its components to
`operational`. An update is public when the incident is, unless it says
otherwise. `setImpacts` replaces the affected components. Opening, updating,
posting and writing the postmortem need `incidents.write`.

## Postmortems [#postmortems]

```ts
await incidents
  .savePostmortem(incident.id, {
    summary: "Replica lag caused failed writes.",
    rootCause: "A replica on an undersized disk",
    actionItems: [{ title: "Alert on replica lag", ownerId }],
  })
  .orThrow();
await incidents.publishPostmortem(incident.id).orThrow();
```

A postmortem can be drafted at any time and is published once the incident
is resolved, with `incidents.manage`. The status page shows only its summary.

## Status page [#status-page]

```ts
await incidents
  .saveStatusPage(organizationId, {
    slug: "acme",
    title: "Acme status",
    published: true,
  })
  .orThrow();

const page = await incidents.publicStatusPage("acme", 30).orThrow();
```

`publicStatusPage(slug, days)` works with the publishable key, signed in or
not. It returns `undefined` unless the page is published. Otherwise it
returns the page title and description, the overall status, each public
component with its status, `uptimePercent` and one entry per day (1 to 90
days, default 90) with its worst status and minutes of outage, and the public
incidents that are open or resolved in the window. Each incident carries its
title, severity, status, start and end, the public components it affects, its
public updates (newest first) and the summary of a published postmortem.
Descriptions, commanders, private updates, private components, root causes,
action items and ids of organizations and members never appear.

`publicStatusPage` is the only function `anon` may call. The pgTAP test in
`supabase/tests/database/incidents.test.sql` checks that, and that a private
incident never shows on the page.

| Method                                  | Returns                                       |
| --------------------------------------- | --------------------------------------------- |
| `saveComponent(organizationId, input)`  | the component                                 |
| `deleteComponent(componentId)`          | its id                                        |
| `components(organizationId)`            | the components in order                       |
| `open(organizationId, input)`           | the incident                                  |
| `update(incidentId, changes)`           | the incident                                  |
| `postUpdate(incidentId, input)`         | the update                                    |
| `setImpacts(incidentId, impacts)`       | the incident                                  |
| `get(incidentId)`                       | the incident with its timeline and postmortem |
| `list(organizationId, options)`         | the incidents, newest first                   |
| `savePostmortem(incidentId, input)`     | the postmortem                                |
| `publishPostmortem(incidentId)`         | the published postmortem                      |
| `saveStatusPage(organizationId, input)` | the page                                      |
| `statusPage(organizationId)`            | the organization's page, or `undefined`       |
| `publicStatusPage(slug, days)`          | the public page, or `undefined`               |

## Errors [#errors]

| Hint                         | Cause                                                                                      |
| ---------------------------- | ------------------------------------------------------------------------------------------ |
| `INCIDENTS_FORBIDDEN`        | the caller lacks `incidents.read`, `.write` or `.manage`                                   |
| `INCIDENTS_INVALID`          | a field is invalid, such as a slug that isn't 2 to 63 lowercase letters, digits and dashes |
| `INCIDENT_NOT_FOUND`         | no incident with this id in an organization the caller reads                               |
| `STATUS_COMPONENT_NOT_FOUND` | no component with this id                                                                  |
| `POSTMORTEM_NOT_FOUND`       | the incident has no postmortem                                                             |
| `INCIDENT_NOT_RESOLVED`      | the postmortem of an unresolved incident can't be published                                |
| `STATUS_PAGE_SLUG_TAKEN`     | another organization uses this slug                                                        |

## Events [#events]

`incident.opened`, `incident.updated`, `incident.resolved`,
`postmortem.published` and `status_component.changed`. The
[events page](/docs/extending/events) lists their payloads.