Skip to content

Project events and synchronisation

Every change that matters to a reader of a project — a well or wellbore created, identified, shared or associated; a revision uploaded or published; access given or taken away; a review stage, result group, source selection or subscription changed — is recorded as one event in that project’s log, in the same transaction as the change. An application keeps itself in step by reading the log after a cursor.

An event never carries the state to trust. It names a subject (subject_kind, subject_id) and the application reads that subject again through the ordinary routes. Receiving an event grants nothing.

changes/head ──> {epoch, cursor} take a starting point
│
▼
changes/list {epoch, after, limit} ──> events after the cursor that you may see now,
│ the highest cursor scanned, has_more
▼
re-read each named subject ──> save the checkpoint (epoch, cursor) only after they landed
│
└── 409 CURSOR_EXPIRED ──> resynchronise (below), then continue from the new head

changes/stream delivers the same events as server-sent events: one change frame per poll whose id is epoch:cursor — also when nothing new is visible — so a standard client’s Last-Event-ID resumes exactly. A connection lasts at most five minutes; reconnect with the last id. Access is checked again on every poll: a subject you lose stops appearing and the stream continues; losing the project ends it.

Visibility is decided when the log is read, not when it was written.

Events Seen by
entity-created, entity-identified, entity-shared, association-added, association-removed, dependent-view-invalidated people who may read that well or wellbore now
revision-appended, read-access-granted, reuse-access-granted, stage-changed, item-withdrawn, distribution-policy-changed people who may read that asset now
access-lost, entity-access-lost, reuse-access-revoked, result-group-visibility-lost only the person who lost it
result-group-changed the group’s owner and anyone who can open a member now
result-group-deleted the people who could see the group when it was deleted
source-bound, source-paused, source-resumed, source-removed, source-reviewed, source-revision-detected, source-revision-advanced, source-state-changed the selection’s owner
subscription-changed that subscriber
approval-requested the person whose agent asks
access-block-changed project administrators
stage-names-changed, external-snapshot-appended, catalog-registration-promoted project members

An event about something you cannot see is omitted, never counted. A page with nothing visible still advances your cursor.

The Python SDK does the whole recipe:

sync = client.sync("state/checkpoint.json")
sync.run() # a full resynchronisation the first time, then catch-up only
for event in sync.follow():
sync.run()

Resynchronisation. Capture changes/head; enumerate the paginated inventories (entities/list, catalog/inventory — every logical asset you may read at its head — and result-groups/list with a cursor); build the copy aside and swap it in at once, stamped with the head you captured first; then replay the log from that head. A write that landed while you enumerated is in the replay.

The rules that keep the copy correct.

  • The order of changes is the event cursor. Do not compare revisions or generations across kinds.
  • Re-read one subject at a time. If an event for it arrives while a read is in flight, read it again.
  • A read that started before a resynchronisation swap is discarded.
  • A copy belongs to one project, person, capability set and epoch; any change to those starts over.
  • An access-lost that arrives late is a reason to re-read, not to delete: a successful read keeps it.
  • Advance the checkpoint only after every read for a page has landed. A crash repeats the page.

A project administrator can ask Ophiolite to notify an address when the project changes (Settings → Notifications, or webhooks/create). The address must be https:// and on the public internet; Ophiolite checks where the name points when you save it and delivers to that address only.

Each change is a POST of one event (the same fields as changes/list) with these headers:

  • X-Ophiolite-Delivery: <project>:<epoch>:<cursor> — the same change always has the same key. Delivery is at least once: keep the keys you have applied and ignore a repeat.
  • X-Ophiolite-Signature: t=<unix seconds>,v1=<hex> — HMAC-SHA256, keyed with the secret shown once when the webhook was created, over the bytes <t>. followed by the request body. Recompute it and compare before trusting the body; reject old timestamps.
  • X-Ophiolite-Event: <kind>.

Changes arrive in order; a change the person who created the webhook may not see is never sent. Answer with any 2xx status to accept. Anything else — a redirect included, which is never followed — is retried after 10 s, then 20 s, 40 s and so on up to 5 minutes; after ten failures in a row the webhook is paused with a reason in plain words, and resumes from the same change when you resume it. webhooks/deliveries shows the log; webhooks/replay delivers again from a kept cursor. After a restore or a move, or if the receiver fell behind what the log keeps, the next delivery is one resync-required naming the current head: re-read the project’s current state, then continue.

Events are kept for 30 days (the deployment may change this). A cursor older than what is kept, or from another copy of the project — after a restore, an import or a move, which start a new epoch — is refused with 409 CURSOR_EXPIRED; resynchronise. The inbox is not affected by retention: its notices are kept with the change that created them.

The release that introduces the log also moves the inbox onto it. At its first start the gateway copies every existing inbox notice, with its read state, before it serves anything; nothing changes for people. Earlier releases’ catalog/changes feed is unchanged.