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.
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.
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.
| Field | Type | Description |
|---|---|---|
id | UUID | Event identifier. |
projectId | string | null | Project scope. null in OSS / single-tenant deployments. |
envId | UUID | Environment the drifted entity lives in. |
entityId | UUID | The drifted entity. |
entityName | string | null | Joined-in entity name (natural key). |
entityTypeName | string | null | Joined-in entity type name. |
fieldPath | string | The drifted property key. Top-level keys today; nested paths are future work. |
catalogValue | JSON | The value Terrantula's catalog held at detection time. |
actualValue | JSON | The value the reconciler observed in the source. |
source | string | The source tag that detected it, e.g. bare-tf:prod.tfstate. |
status | enum | open, accepted, reapplied, or snoozed. See below. |
snoozedUntil | ISO timestamp | null | When a snoozed event re-surfaces. null otherwise. |
detectedAt | ISO timestamp | When the divergence was first detected. |
updatedAt | ISO timestamp | Last 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.
| Status | Meaning | How it's reached |
|---|---|---|
open | Active, unresolved drift. The default for a freshly detected divergence. | The reconciler detects a field diff. |
accepted | The observed value was promoted into the catalog. | POST .../accept, or the reconciler auto-closes it when the field stops drifting. |
reapplied | Intent recorded to push the catalog value back out to infrastructure. | POST .../reapply. The actual push flows through the apply pipeline. |
snoozed | Temporarily hidden until snoozedUntil. | POST .../snooze. |
When an operator resolves an open event they choose a direction:
| Policy | What it does | Use 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 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.
A background worker sweeps configured sources on a fixed cadence (default
15 minutes; overridable per project via projects.metadata.driftIntervalSeconds).
For each source it:
properties,
comparing top-level keys (deep-equal by value).open event per drifted field. Re-detecting the same drift
refreshes actualValue and detectedAt on the existing open row.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.
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.
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.
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.
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | /drift-events | data:read | List 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/count | data:read | Count of open events in the environment. |
GET | /drift-events/:id | data:read | Fetch one event. 404 if not found. |
POST | /drift-events/:id/accept | data:write | Promote actualValue into the catalog; mark accepted. 404 if no matching open event. |
POST | /drift-events/:id/reapply | data:write | Record reapply intent; mark reapplied; return the entity payload. |
POST | /drift-events/:id/snooze | data:write | Snooze 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.
The TypeScript SDK exposes the same surface through driftEvents:
The client mirrors the API: list, count, get, accept, reapply, and
snooze.
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.
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.
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.
data:read / data:write gates on this surface.properties that drift is detected against.