See it in action on the hev-shop demo store.

API

Snapshot History

Snapshots are materialized facet histograms for a namespace. They carry facet listings in values[].v and facet counts in values[].n, stored durably in S3 and mirrored into Aerospike for the latest body.

Use POST /snapshots to materialize a field now. Use history and body routes to read the durable chronology written by the consistency watcher.

Snapshot policy

Configure automatic snapshot writes on the namespace’s Index CR:

apiVersion: hevlayer.com/v1
kind: Index
metadata:
  name: products
spec:
  backend:
    namespace: products
  snapshot:
    interval: 5m
    retention: 30d
    facetFields:
      - category
      - brand
FieldDefaultBehavior
facetFields[]Facet fields to histogram. Empty or unset disables the automatic snapshot writer for the namespace, so history and activity stay empty.
interval5mMinimum spacing between automatic snapshot writes. The writer fires on each upstream-stable advance; interval only floors how often a write lands. The gateway fallback is LAYER_SNAPSHOT_MIN_INTERVAL_MS.
retentionnevernever keeps all history. A duration such as 30d prunes S3 bodies older than the window, while always keeping the most recent body.

Snapshots are event-driven, not scheduled: an idle namespace does not get a new snapshot just because interval elapsed. The gateway refreshes Index policy periodically, so edits take effect without a pod restart.

Manual POST /snapshots jobs with source: origin and the automatic writer use the same shard fan-out path. Origin work is bounded by spec.scan.threads; stored and cache snapshot reads do not fan out.

Routes

RouteMethodBehavior
POST /v2/namespaces/{ns}/snapshotsPOSTCreate an on-demand snapshot job for one field.
GET /v2/namespaces/{ns}/snapshot-jobsGETList in-memory snapshot jobs.
GET /v2/namespaces/{ns}/snapshot-jobs/{id}GETRead one snapshot job.
GET /v2/namespaces/{ns}/historyGETNewest-first durable snapshot history.
GET /v2/namespaces/{ns}/snapshots/{sha}GETFull snapshot body by full SHA or 7-char prefix.
GET /v2/activity/snapshotsGETCross-namespace snapshot-write activity stream.

Manual snapshot

job = await client.create_snapshot("products", {
    "field": "category",
    "source": "auto",
    "filters": ["brand", "Eq", "Acme"],
    "page_size": 1000,
})
job, err := client.CreateSnapshot(ctx, "products", &hevlayer.CreateSnapshotRequest{
    Field:    "category",
    Source:   "auto",
    Filters:  []interface{}{"brand", "Eq", "Acme"},
    PageSize: 1000,
})
const job = await client.createSnapshot("products", {
  field: "category",
  source: "auto",
  filters: ["brand", "Eq", "Acme"],
  page_size: 1000,
});
curl -X POST "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshots" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "field": "category",
    "source": "auto",
    "filters": ["brand", "Eq", "Acme"],
    "page_size": 1000
  }'

Valid sources are auto, stored, cache, and origin.

SourceReads fromNotes
autoStored snapshot when possible, otherwise cache/origin policyDefault. Stored snapshots only support unfiltered configured fields.
storedLatest S3 snapshot body, with Aerospike mirror as a cacheFastest path for configured facet fields.
cacheAerospike document cacheSupports filters the cache can evaluate.
originTurbopuffer paginated scanAuthoritative. Persists the computed snapshot body to S3.

The response is 202 Accepted:

{
  "id": "snapshot-job-uuid",
  "namespace": "products",
  "field": "category",
  "source": "auto",
  "status": "running",
  "progress": 0,
  "documents_scanned": 0,
  "created_at": "2026-05-26T10:00:00Z"
}

Poll the job:

job = await client.get_snapshot_job("products", job.id)
job, err := client.GetSnapshotJob(ctx, "products", jobID)
const job = await client.getSnapshotJob("products", jobId);
curl "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshot-jobs/snapshot-job-uuid" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY"

Completed jobs include sha when a body was materialized:

{
  "id": "snapshot-job-uuid",
  "namespace": "products",
  "field": "category",
  "source": "origin",
  "status": "completed",
  "documents_scanned": 12844,
  "sha": "3f9e8b21",
  "stable_as_of": 1747300000123
}

History

history = await client.list_namespace_history("products", limit=20)
history, err := client.ListNamespaceHistory(ctx, "products",
    &hevlayer.ListNamespaceHistoryParams{Limit: 20})
const history = await client.listNamespaceHistory("products", { limit: 20 });
curl "$LAYER_GATEWAY_URL/v2/namespaces/products/history?limit=20" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY"
[
  {"watermark_ms": 1747300000123, "sha": "3f9e8b21..."},
  {"watermark_ms": 1747299600045, "sha": "a1c5b09f..."}
]
Query paramDefaultPurpose
limit50Maximum entries returned. Capped at 500.
beforenoneReturn entries older than this SHA. 7-char prefixes are accepted.

The history endpoint lists S3 keys only; it does not read every snapshot body.

Snapshot body

body = await client.get_namespace_snapshot("products", "3f9e8b2")
body, err := client.GetNamespaceSnapshot(ctx, "products", "3f9e8b2")
const body = await client.getNamespaceSnapshot("products", "3f9e8b2");
curl "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshots/3f9e8b2" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY"
{
  "namespace": "products",
  "watermark_ms": 1747300000123,
  "sha": "3f9e8b21",
  "row_count": 12500,
  "fields": [
    {
      "name": "category",
      "values": [
        {"v": "books", "n": 1240},
        {"v": "electronics", "n": 873}
      ]
    }
  ],
  "fields_skipped": [
    {
      "name": "tags",
      "reason": "exceeded_cap",
      "distinct_observed": 247000,
      "cap": 10000
    }
  ]
}

fields[].values[].v is the facet listing. fields[].values[].n is the facet count. row_count is the number of rows scanned into the snapshot; for vector namespaces, namespace metadata compares it with the upstream namespace row count to report indexed and index_lag_rows. Fields present in fields[] are complete. Fields above the 10,000 distinct-value cap are listed in fields_skipped[] instead of being partially materialized. A skipped field is still enumerable on demand with a values scan, which carries a 1,000,000-value cap instead.

Activity

activity = await client.list_snapshot_activity(since=1747200000000, limit=50)
activity, err := client.ListSnapshotActivity(ctx,
    &hevlayer.ListSnapshotActivityParams{Since: 1747200000000, Limit: 50})
const activity = await client.listSnapshotActivity({
  since: 1747200000000,
  limit: 50,
});
curl "$LAYER_GATEWAY_URL/v2/activity/snapshots?since=1747200000000&limit=50" \
  -H "Authorization: Bearer $LAYER_GATEWAY_API_KEY"
Query paramRequiredPurpose
sinceyesEpoch-ms lower bound on ts_ms.
limitnoCap 500, default 50.
namespacenoExact namespace filter.
cursornoPagination cursor from next_cursor.

Activity is snapshot lifecycle only. Search history and clickstream events have separate feeds.

esc