Drift events

A drift event records that one field of an entity's stored properties diverges from what a configured reconciler observed in an external source. The reconciler runs periodically, writes one event per drifted (entity, field), and surfaces them for an operator to accept, reapply, or snooze.

This is the lookup reference for the drift_events record, the event lifecycle, the reconciler's behavior, and the API/SDK surface.

This is internal drift, not cross-source drift

Drift events compare an entity's stored properties against what a reconciler observes inside Terrantula. That is distinct from cross-source drift, which tracks an entity against the external IaC source it was imported from (a TFC state file, an Atmos stacks dir, an S3 state). They use different tables and different APIs. For the external-source flow see the Cross-source drift how-to.

Event fields

Each drift event the API returns has these fields. The list/get response joins in the entity's name and entityTypeName for convenience; the rest map directly to the drift_events row.

FieldTypeDescription
idUUIDEvent identifier.
projectIdstring | nullProject scope. null in OSS / single-tenant deployments.
envIdUUIDEnvironment the drifted entity lives in.
entityIdUUIDThe drifted entity.
entityNamestring | nullJoined-in entity name (natural key).
entityTypeNamestring | nullJoined-in entity type name.
fieldPathstringThe drifted property key. Top-level keys today; nested paths are future work.
catalogValueJSONThe value Terrantula's catalog held at detection time.
actualValueJSONThe value the reconciler observed in the source.
sourcestringThe source tag that detected it, e.g. bare-tf:prod.tfstate.
statusenumopen, accepted, reapplied, or snoozed. See below.
snoozedUntilISO timestamp | nullWhen a snoozed event re-surfaces. null otherwise.
detectedAtISO timestampWhen the divergence was first detected.
updatedAtISO timestampLast status change.

There is at most one open event per (entity, fieldPath) — a partial unique index enforces it, so a re-detection of the same drift updates the existing open row rather than piling up duplicates.

Statuses

StatusMeaningHow it's reached
openActive, unresolved drift. The default for a freshly detected divergence.The reconciler detects a field diff.
acceptedThe observed value was promoted into the catalog.POST .../accept, or the reconciler auto-closes it when the field stops drifting.
reappliedIntent recorded to push the catalog value back out to infrastructure.POST .../reapply. The actual push flows through the apply pipeline.
snoozedTemporarily hidden until snoozedUntil.POST .../snooze.

Reconciliation policies — accept vs. reapply

When an operator resolves an open event they choose a direction:

PolicyWhat it doesUse it when…
Accept (POST .../accept)Promotes actualValue into the entity's properties for that fieldPath, then marks the event accepted. A direct write to Terrantula's own catalog — never to real infrastructure.The source is right and the catalog was stale — you want the graph to match what's actually deployed.
Reapply (POST .../reapply)Records intent to push catalogValue back out to infrastructure and marks the event reapplied. It returns the entity payload so a client can route the operator to the right Action. The catalog is left unchanged.The catalog is right and the source drifted — you want infrastructure to match the intended state.
Reapply does not run anything

reapply only records intent. Terrantula never runs terraform apply. The actual push happens through the apply pipeline — typically a Terrantula Action that opens a pull request— which your CI then applies. The endpoint returns the entity so a client can route the operator to that Action.

The reconciler

A background worker sweeps configured sources on a fixed cadence (default 15 minutes; overridable per project via projects.metadata.driftIntervalSeconds). For each source it:

  1. Observes the source's view of every entity it covers.
  2. Diffs each observation against the matching catalog entity's properties, comparing top-level keys (deep-equal by value).
  3. Upserts one open event per drifted field. Re-detecting the same drift refreshes actualValue and detectedAt on the existing open row.
  4. Auto-closes open events for that entity whose field no longer drifts — they move to accepted (catalog and actual agreed at observe time, so no operator action is needed).

The reconciler is advisory-locked so it never overlaps across server pods.

Sources

The shipped source kind is bare-tf: it reads a Terraform state file from a sandboxed, project-scoped path and maps resources to entities via a type map. Configured under a project's metadata.drift.sources. An OSS / single-tenant deployment can point the reconciler at one state file with the TERRANTULA_OSS_TFSTATE_PATH environment variable, with no projects table involved.

No SaaS dependency

The reconciler is part of the Apache 2.0 backend and reads a local state file — no hosted service is required. This keeps the OSS-first deployment fully self-hostable. See the Configuration Referencefor the drift config keys and environment variables.

API surface

Drift events are environment-scoped, under /:org/:project/envs/:envName/drift-events. Reads require data:read (every role); the resolution POSTs require data:write (member and above) — see RBAC roles & permissions.

MethodPathPermissionPurpose
GET/drift-eventsdata:readList events. Filters: status, kind (entity type), entityId (entity name), since (ISO lower bound on detectedAt), limit (1–500, default 100). Newest first.
GET/drift-events/countdata:readCount of open events in the environment.
GET/drift-events/:iddata:readFetch one event. 404 if not found.
POST/drift-events/:id/acceptdata:writePromote actualValue into the catalog; mark accepted. 404 if no matching open event.
POST/drift-events/:id/reapplydata:writeRecord reapply intent; mark reapplied; return the entity payload.
POST/drift-events/:id/snoozedata:writeSnooze for untilSeconds (default 1 day, capped at 30 days).

The resolution endpoints act only on open events — accepting, reapplying, or snoozing an already-resolved event returns 404.

Full request/response schemas are in the generated Drift Events API reference.

SDK

The TypeScript SDK exposes the same surface through driftEvents:

import { createClient } from '@terrantula/sdk'

const client = createClient('https://your-terrantula-host', 'your-api-token')

// List open drift events for an env
const open = await client.driftEvents.list({
  orgId: 'acme',
  projectId: 'fleet',
  envName: 'prod',
  status: 'open',
})

// Resolve one by accepting the observed value into the catalog
await client.driftEvents.accept({
  orgId: 'acme',
  projectId: 'fleet',
  envName: 'prod',
  id: open[0].id,
})

// Or snooze it for 6 hours
await client.driftEvents.snooze({
  orgId: 'acme',
  projectId: 'fleet',
  envName: 'prod',
  id: open[0].id,
  untilSeconds: 6 * 60 * 60,
})

The client mirrors the API: list, count, get, accept, reapply, and snooze.

Caveats

Drift detection is read-only; the UI can't write the fix

The reconciler observes; it never mutates infrastructure. Likewise the graph UI is a read-only projection — you can't close a drift event by editing an entity's properties in the dashboard. Resolve it through the drift API (accept / reapply / snooze), and let intended infrastructure changes flow through an Action's PR.

Diffing is top-level only today

The reconciler compares top-level property keys. Nested JSON-pointer paths are future work. A drift in a nested object surfaces as a diff on its top-level key.

Resolution acts only on open events

accept, reapply, and snooze require the event to be open. Calling them on an already-accepted, reapplied, or snoozed event returns 404. List with status=opento find resolvable events.