Arke
Build

Events & Sync

How Arke's event system works for real-time sync and change tracking.

Overview

Arke emits events when entities are created or updated. Events are stored in D1 and provide a rolling-window log for real-time client synchronization. This is a lightweight sync mechanism, not a permanent audit trail -- the tips table is the source of truth.

Events are ephemeral with a 30-day rolling window. For full sync, use snapshots.

Event Structure

Each event contains:

FieldTypeDescription
idnumberAuto-increment event ID (use as cursor)
pistringEntity ID that changed
cidstringNew manifest CID
tsstringISO timestamp of the event

Example event:

{
  "id": 12346,
  "pi": "01KDETYWYWM0MJVKM8DK3AEXPY",
  "cid": "bafyreibug443cnd4endcwinwttw3c3dzmcl2ikht64xzn5qg56bix3usfy",
  "ts": "2025-01-15T12:00:01Z"
}

Consuming Events

GET /events

No authentication required -- the events endpoint is public.

Query Parameters

ParameterTypeDescription
cursornumberReturn events older than this ID (from previous response)
limitnumberMaximum events to return (default: 100, max: 1000)
networkstringNetwork to query: main or test (default: main)

Response Format

{
  "events": [
    { "id": 12346, "pi": "01KDE...", "cid": "bafyrei...", "ts": "2025-01-15T12:00:01Z" },
    { "id": 12345, "pi": "01KAB...", "cid": "bafyrei...", "ts": "2025-01-15T12:00:00Z" }
  ],
  "has_more": true,
  "cursor": 12345
}
  • events: List of events, newest first
  • has_more: Whether there are more (older) events available
  • cursor: Pass this as ?cursor= to get older events

Sync Flow

The events endpoint uses cursor-based pagination for efficient synchronization:

  1. Initial fetch: GET /events -- get the newest events, save the highest id as your high-water mark
  2. Paginate backwards: GET /events?cursor=X -- get older events until has_more=false
  3. Poll for new: GET /events -- if the newest id > your high-water mark, process new events

Example Sync Implementation

let highWaterMark = 0;

// Initial sync - get all events
async function initialSync() {
  let cursor: number | undefined;

  do {
    const url = cursor ? `/events?cursor=${cursor}` : '/events';
    const res = await fetch(url);
    const data = await res.json();

    // Update high-water mark from first batch
    if (!cursor && data.events.length > 0) {
      highWaterMark = data.events[0].id;
    }

    // Process events
    for (const event of data.events) {
      await processEvent(event);
    }

    cursor = data.has_more ? data.cursor : undefined;
  } while (cursor);
}

// Poll for new events
async function pollForNew() {
  const res = await fetch('/events');
  const data = await res.json();

  const newEvents = data.events.filter(e => e.id > highWaterMark);
  if (newEvents.length > 0) {
    highWaterMark = newEvents[0].id;
    for (const event of newEvents) {
      await processEvent(event);
    }
  }
}

Event Pruning

Events are automatically pruned after 30 days via an hourly cron job. This keeps the events table lean while providing a sufficient window for client synchronization.

On this page