Incidents

An incident is a technical or business problem that needs an administrator's attention.

Examples are an unknown billing subscription, an invalid order status transition, a failed external integration, or inventory below a critical threshold.

Incidents, exceptions, and logs

An incident is not the same as an exception or a log entry.

  • An exception stops the current operation. Most exceptions are expected, for example a validation error, and need no attention.
  • A log entry records what happened at one moment. Nobody owns it, and it does not change afterwards.
  • An incident is an active problem that is persisted until an administrator resolves it.

The reporting domain decides when a problem is an incident. Vuesion never creates incidents automatically from exceptions.

Recurring problems

Every incident has a globally unique key. The key identifies the same recurring problem, so one problem results in one incident, no matter how often it is reported.

Build the key from the source, the type, and the affected resource, for example orders:invalid_status_transition:01J....

Reporting an incident

Domains report incidents through reportIncident of the incident service:

TypeScript
import { useIncidentService } from '#server/domain/incident/use-incident-service';

await useIncidentService().reportIncident({
  key: `orders:invalid_status_transition:${order.id}`,
  source: 'orders',
  type: 'invalid_status_transition',
  severity: 'ERROR',
  title: 'Invalid order status transition',
  description: 'A shipped order cannot move back to draft.',
  context: { orderId: order.id, from: 'SHIPPED', to: 'DRAFT' },
});
  • If no incident with the key exists, a new incident is created with one occurrence.
  • If the incident exists, its ID and firstSeenAt are kept. The occurrences are incremented, lastSeenAt is updated, and the severity, title, description, and context are replaced with the new report. An omitted description or context becomes null.

Reporting is a single atomic upsert, so concurrent reports of the same key neither create duplicates nor lose occurrences.

The service does not catch database errors. The caller decides whether reporting is best effort, for example by catching and logging a failed report so that its own work can continue.

Like other services, the incident service accepts a transaction client, useIncidentService(tx). Incidents reported inside a transaction are rolled back with it.

Resolving an incident

resolveIncident(id) permanently deletes the incident, including its diagnostic context. No history is kept.

Resolving is idempotent: resolving an incident that no longer exists succeeds and changes nothing.

When the problem is reported again, a new incident is created with one occurrence.

Context

The context is a JSON object with diagnostic information. Each domain defines its own context, and there is no fixed schema:

JSON
{ "integration": "erp", "endpoint": "inventory-sync", "httpStatus": 503 }

Incidents have no foreign keys to other models. Reference resources by their IDs in the context.

The incident service cannot know whether arbitrary JSON contains secrets. The caller is responsible for supplying safe data:

  • Never include API keys, access tokens, passwords, signed references, payment details, or complete provider responses.
  • Avoid personal data such as email addresses. Store an ID and look up the details when they are needed. Include personal data only when an administrator needs it to investigate the problem, as billing does with the customer email of an unknown subscription.

Billing incidents

Billing reports incidents for problems that need an administrator, with the source billing and the severity ERROR. They supplement the existing billing warnings in the server logs, which are still written.

TypeKeyReported when
unknown_subscriptionbilling:unknown-subscription:<subscriptionId>The reconciliation detects an unknown provider subscription
workspace_not_foundbilling:sync:workspace_not_found:<subscriptionId>The workspace of a subscription does not exist
subscription_conflictbilling:sync:subscription_conflict:<subscriptionId>A subscription would become current while another one is current
unknown_pricebilling:sync:unknown_price:<subscriptionId>No plan price uses the provider price of a subscription

<subscriptionId> is the External ID of the provider subscription. The synchronization types match the reasons of the synchronization warnings, and different problems of one subscription are separate incidents.

The context contains the available identifiers: provider (lemonsqueezy or demo), subscriptionId, externalPriceId, and providerStatus (the mapped Vuesion status). Synchronization incidents add the reason and the workspaceId, and a conflict adds the currentSubscriptionId. A webhook for a deleted workspace has no provider state, so its context only contains the provider, the subscription, the reason, and the workspace. An unknown subscription has no workspace, because only the signed billing reference of its webhook proves it; its context contains the customerEmail when the provider listed one. The context never contains provider responses, webhook payloads, billing references, or payment data.

Billing does not report incidents for invalid webhook signatures, invalid payloads, missing or invalid billing references, or transient provider failures such as timeouts, network errors, or HTTP 429, 502, and 503. Those are only logged or returned to the provider, which retries the webhook; the next reconciliation run retries known subscriptions.

Incidents are reported after the failed billing transaction, so they are not rolled back with it. Reporting is best effort: a failed report is logged with the incident key, and the billing operation, its response status, and the reconciliation of the other subscriptions stay unchanged.

Resolving a billing incident only deletes it. It does not repair the subscription, and a problem that still exists is reported again as a new incident: by the next hourly reconciliation run, or, for a failure only a webhook detects, by the next delivery of the webhook. See the subscription recovery runbook.

Incident Board

Platform admins manage incidents on the Incident Board at /admin/incidents. The Incidents entry in the Platform section of the sidebar is only visible to platform admins, and the /api/admin/incidents endpoints reject all other users.

The board lists all incidents by lastSeenAt, newest first, and can be filtered by:

  • Severity: info, warning, error, critical, or all
  • Source: part of the source, ignoring case
  • Search: part of the title

Expand a row to investigate an incident. The details are loaded when the row is expanded and show all metadata, the full description, and the diagnostic context. Top-level context values are listed as key-value pairs, and nested objects and arrays are shown as formatted JSON. The context is read-only and is always rendered as text.

Incidents can be resolved from the expanded row, which deletes them.

The board translates its labels and the severity values. Incident content, such as the title, description, source, type, key, and context, is displayed exactly as reported. Vuesion's own integrations report incidents in English, and applications can report incidents in any language.

Next steps

Continue with Development Workflow to learn how new features are typically built inside Vuesion and how the different tools and concepts come together during everyday development.