Skip to content

Events

DocStore emits structured events for every significant mutation. Events use the CloudEvents 1.0 envelope format and are delivered via two mechanisms:

  • SSE streams — real-time, in-memory, best-effort delivery over a persistent HTTP connection.
  • Webhook subscriptions — durable delivery to an HTTPS endpoint, with automatic retries and HMAC signing.

Event envelope

Every event is a CloudEvents 1.0 JSON object:

{
  "specversion": "1.0",
  "type": "com.docstore.commit.created",
  "source": "/repos/acme/platform",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "time": "2024-06-01T12:00:00Z",
  "datacontenttype": "application/json",
  "data": { ... }
}
Field Type Description
specversion string Always "1.0".
type string Event type string (see table below).
source string Resource path that emitted the event (e.g. /repos/acme/platform).
id string UUID, unique per event.
time RFC3339 UTC timestamp of when the event was emitted.
datacontenttype string Always "application/json".
data object Event-specific payload (see table below).

Event types

Event type Trigger Key payload fields
com.docstore.repo.created Repo created repo, owner, created_by
com.docstore.repo.deleted Repo deleted repo, deleted_by
com.docstore.commit.created Commit written to a branch repo, branch, sequence, author, message, file_count
com.docstore.branch.created Branch created repo, branch, base_sequence, created_by
com.docstore.branch.merged Branch merged into main repo, branch, sequence, merged_by
com.docstore.branch.rebased Branch rebased onto main repo, branch, new_base_sequence, new_head_sequence, commits_replayed, rebased_by
com.docstore.branch.abandoned Branch marked abandoned repo, branch, abandoned_by
com.docstore.check.reported CI check result posted repo, branch, sequence, check_name, status, reporter
com.docstore.merge.blocked Merge attempted but blocked by policy repo, branch, actor, policies
com.docstore.proposal.opened Proposal opened on a branch repo, branch, base_branch, proposal_id, author, sequence
com.docstore.proposal.closed Proposal closed without merge repo, branch, proposal_id
com.docstore.proposal.merged Proposal merged repo, branch, base_branch, proposal_id
com.docstore.review.submitted Review submitted on a branch repo, branch, sequence, reviewer, status
com.docstore.org.created Org created org, created_by
com.docstore.org.deleted Org deleted org, deleted_by
com.docstore.role.changed Repo role granted or revoked repo, identity, role, changed_by

Payload field reference

Commit created (com.docstore.commit.created):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "sequence": 43,
  "author": "alice@example.com",
  "message": "add retry logic",
  "file_count": 2
}

Branch merged (com.docstore.branch.merged):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "sequence": 44,
  "merged_by": "alice@example.com"
}

Branch rebased (com.docstore.branch.rebased):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "new_base_sequence": 44,
  "new_head_sequence": 46,
  "commits_replayed": 2,
  "rebased_by": "alice@example.com"
}

Merge blocked (com.docstore.merge.blocked):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "actor": "alice@example.com",
  "policies": ["require_review", "ci_must_pass"]
}

Check reported (com.docstore.check.reported):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "sequence": 43,
  "check_name": "ci/build",
  "status": "passed",
  "reporter": "ci-worker@system"
}

Review submitted (com.docstore.review.submitted):

{
  "repo": "acme/platform",
  "branch": "feature/add-retry",
  "sequence": 43,
  "reviewer": "bob@example.com",
  "status": "approved"
}

Role changed (com.docstore.role.changed):

{
  "repo": "acme/platform",
  "identity": "bob@example.com",
  "role": "writer",
  "changed_by": "alice@example.com"
}

When a role is revoked rather than granted, role is an empty string ("").


SSE streaming

DocStore exposes two Server-Sent Events endpoints for real-time event delivery. SSE delivery is in-memory and best-effort — disconnected clients miss events that were emitted while they were away. For durable delivery, use webhook subscriptions.

Multi-instance note: SSE fan-out is in-process. If you run more than one server instance (--max-instances > 1), a client is only guaranteed to receive events from the instance it is connected to. For reliable delivery across instances, use webhooks.

Repo-scoped stream

GET /repos/{name}/-/events

Authorization: Reader+ on the repo.

Query parameters:

Parameter Description
types Comma-separated list of event types to receive. Omit to receive all types.

The server sends one SSE message per event:

data: {"specversion":"1.0","type":"com.docstore.commit.created",...}

data: {"specversion":"1.0","type":"com.docstore.branch.merged",...}

A keepalive comment (: keepalive) is sent every 15 seconds to prevent proxies from closing the connection.

Example — stream all events on a repo:

curl -N -H "Authorization: Bearer $TOKEN" \
  https://docstore.example.com/repos/acme/platform/-/events

Example — stream only commit and merge events:

curl -N -H "Authorization: Bearer $TOKEN" \
  "https://docstore.example.com/repos/acme/platform/-/events?types=com.docstore.commit.created,com.docstore.branch.merged"

Global stream

GET /events

Authorization: Global admin only.

Query parameters:

Parameter Description
repo Exact repo name to filter (e.g. acme/platform). Omit to receive events from all repos.

Example — stream all events across all repos:

curl -N -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://docstore.example.com/events

Example — filter to a single repo:

curl -N -H "Authorization: Bearer $ADMIN_TOKEN" \
  "https://docstore.example.com/events?repo=acme/platform"

Webhook subscriptions

Webhook subscriptions provide durable, retried delivery to an HTTPS endpoint. The server writes each matching event to a transactional outbox and delivers it asynchronously.

Authorization

Operation Required role
Create subscription (repo-scoped, repo field set) Reader+ on the target repo
Create subscription (global, repo field omitted) Global admin
List subscriptions Global admin (all) or any authenticated user (own)
Delete subscription Global admin or subscription creator
Resume a suspended subscription Global admin or subscription creator

Event filtering

A subscription receives events based on two optional filters. Both must match for an event to be delivered:

Filter field Behavior when omitted Behavior when set
repo Receives events from all repos Receives events only from the named repo
event_types Receives all event types Receives only the listed event types

HMAC-SHA256 request signing

Every webhook delivery is an HTTP POST with Content-Type: application/cloudevents+json.

When a subscription is created with a non-empty secret in its config, every webhook delivery also includes an X-DocStore-Signature header:

X-DocStore-Signature: sha256=<hex-encoded-hmac-sha256>

The signature is computed over the raw request body:

HMAC-SHA256(key=secret, message=body)

Verification example (Go):

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verify(body []byte, secret, header string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(header), []byte(expected))
}

Verification example (Python):

import hmac, hashlib

def verify(body: bytes, secret: str, header: str) -> bool:
    sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    expected = f"sha256={sig}"
    return hmac.compare_digest(header, expected)

Always use a constant-time comparison (hmac.Equal / hmac.compare_digest) to prevent timing attacks.

Secret storage: The secret value is stored and returned in plaintext. To rotate a secret, delete the subscription and recreate it with a new secret value.

Retry and suspend policy

Behavior Detail
HTTP timeout 10 seconds per delivery attempt
Success condition HTTP response status 200–299
Retry backoff Exponential: 1 s, 2 s, 4 s, 8 s, …, capped at 1 hour
Max attempts 10 per outbox row
Auto-suspend After 10 failed attempts the subscription is suspended and no further events are sent
failure_count field Counts the number of times the subscription has been auto-suspended, not the total number of individual failed delivery attempts
Resume POST /subscriptions/{id}/resume clears the suspension and resets the failure counter
Outbox retention Delivered rows are deleted after 7 days

Managing subscriptions

Create a subscription

POST /subscriptions
Content-Type: application/json

Body:

{
  "repo": "acme/platform",
  "event_types": ["com.docstore.commit.created", "com.docstore.branch.merged"],
  "backend": "webhook",
  "config": {
    "url": "https://hooks.example.com/docstore",
    "secret": "my-hmac-secret"
  }
}
  • repo — Optional. Omit to create a global subscription that receives events from all repos.
  • event_types — Optional. Omit to receive all event types.
  • secret — Optional. When set, deliveries are signed with X-DocStore-Signature.

Response 201:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "repo": "acme/platform",
  "event_types": ["com.docstore.commit.created", "com.docstore.branch.merged"],
  "backend": "webhook",
  "config": {"url": "https://hooks.example.com/docstore", "secret": "my-hmac-secret"},
  "created_at": "2024-06-01T12:00:00Z",
  "created_by": "alice@example.com",
  "failure_count": 0
}

List subscriptions

GET /subscriptions

Response 200:

{
  "subscriptions": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "repo": "acme/platform",
      "event_types": ["com.docstore.commit.created"],
      "backend": "webhook",
      "config": {"url": "https://hooks.example.com/docstore"},
      "created_at": "2024-06-01T12:00:00Z",
      "created_by": "alice@example.com",
      "failure_count": 0
    }
  ]
}

A non-null suspended_at means the subscription was automatically suspended after 10 failed deliveries. Resume it before new events will be sent.

Delete a subscription

DELETE /subscriptions/{id}

Response 204.

Resume a suspended subscription

POST /subscriptions/{id}/resume

Clears suspended_at and resets failure_count to 0. The subscription immediately re-enters the active delivery queue.

Response 204.

curl examples

Create a global subscription for all commit events:

curl -X POST https://docstore.example.com/subscriptions \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "backend": "webhook",
    "event_types": ["com.docstore.commit.created"],
    "config": {
      "url": "https://hooks.example.com/docstore",
      "secret": "my-hmac-secret"
    }
  }'

Create a repo-scoped subscription for all event types:

curl -X POST https://docstore.example.com/subscriptions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "repo": "acme/platform",
    "backend": "webhook",
    "config": {
      "url": "https://hooks.example.com/acme-platform"
    }
  }'

List all subscriptions:

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://docstore.example.com/subscriptions

Delete a subscription:

curl -X DELETE -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://docstore.example.com/subscriptions/550e8400-e29b-41d4-a716-446655440000

Resume a suspended subscription:

curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://docstore.example.com/subscriptions/550e8400-e29b-41d4-a716-446655440000/resume

ds CLI examples

Subscription management is also available via the ds CLI:

Create a subscription:

# Repo-scoped, specific event types, with HMAC secret
ds subscriptions create \
  --repo acme/platform \
  --event-types com.docstore.commit.created,com.docstore.branch.merged \
  --url https://hooks.example.com/docstore \
  --secret my-hmac-secret

# Global subscription, all event types
ds subscriptions create \
  --url https://hooks.example.com/global

List subscriptions:

ds subscriptions list

Delete a subscription:

ds subscriptions delete <id>

Resume a suspended subscription:

ds subscriptions resume <id>

Raw markdown — machine-readable source for this page.