ServicesRitual

Ritual

N-party synchronized action - the thing locks cannot do. A ritual definition names a set of parts, each bound to a specific actor, plus a simultaneity window: participants arm an attempt and perform their parts, and the server atomically detects whether enough parts landed close enough together, using only server-side clocks. Inverted, the same primitive is a dead-man switch that fires when someone fails to check in.

When to reach for it: two-person production deploy confirmations, co-op game mechanics (four players on four pressure plates), N-of-M founder kill switches, synchronized start signals, dead-man switches with escalation.

When not to: "at most one actor" is a lock - use Distributed State; weighing votes or proposals over rounds belongs to Consensus; a single human approving a request on their own schedule belongs to Human-in-the-Loop.

Concepts

  • Definitions are reusable templates - name, parts, window, policies. Creating one requires a platform admin token (403 Ritual definitions require admin role otherwise), and they are create-only: no update or delete endpoints, version is always 1.
  • Parts are identity-bound. Each part names an assigned_to principal; a perform or check-in only counts when the caller's JWT subject equals it (admins bypass). A ritual is an authenticated ceremony, not a convention.
  • The window slides. window_ms is satisfied when any quorum-sized set of performances spans at most window_ms - an early stray perform does not doom the attempt if enough later ones cluster.
  • Quorum is N-of-M - {"required": 3, "of": 5}; omitted, it defaults to all parts, and of must equal the part count. An optional ordering must list every part exactly once and forces that sequence.
  • An attempt is one live run of a definition, created by arm: armed, then pending as parts land, ending completed or timed_out when the arm TTL expires first (fired is the inverse-mode terminal state). All timestamps are server-assigned - clients never supply clocks.
  • Rehearsal mode runs the full detection pipeline but suppresses completion side effects, and is tagged rehearsal: true in history.

Define a ceremony

define takes the request body from a JSON file. This one is a two-person vault door - two keys, five seconds, all parts required:

{
  "name": "Two-Person Vault Door",
  "description": "Both keyholders must turn their keys within 5 seconds.",
  "parts": [
    { "part_id": "key_a", "assigned_to": "docs-wave" },
    { "part_id": "key_b", "assigned_to": "docs4-other" }
  ],
  "window_ms": 5000,
  "arm_ttl_seconds": 120
}
snug ritual define --file ./vault-door.json   # requires a platform admin token
snug ritual get --def-id BEYQVwcUuXMnNpTNemuX
snug ritual list --query vault --filter mode:eq:forward --sort-by created_at

list accepts the shared search grammar (q, filter, sort_by); filter mode:eq:inverse was verified live. Fetching a definition or attempt, arming, and viewing history are restricted to assigned participants, the definition owner, or an admin - anyone else gets 403 insufficient_permissions. Listing is the exception: it is not participant-scoped and returns full definition bodies to any authenticated caller (verified with a second user), so keep secrets out of names and descriptions.

Run the ceremony

snug ritual arm --def-id BEYQVwcUuXMnNpTNemuX          # opens an attempt
snug ritual countdown --attempt-id zsfDCyRSNrAdvtEVcmLZ --seconds 3
snug ritual perform --attempt-id zsfDCyRSNrAdvtEVcmLZ --part key_a
snug ritual status --attempt-id zsfDCyRSNrAdvtEVcmLZ

arm returns the attempt_id plus server-assigned armed_at_ms and deadline_ms (armed time plus the definition's arm_ttl_seconds, default 300). countdown returns an authoritative window_open_ms a few seconds out - share it with participants as the go-signal so everyone performs against the same clock.

Over HTTP, performing a part is a POST with the enveloped response:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" -d '{"part_id": "key_a"}' \
  http://localhost:4000/api/v1/rituals/attempts/zsfDCyRSNrAdvtEVcmLZ/perform
{
  "status": 200,
  "msg": "OK",
  "data": {
    "attempt_id": "zsfDCyRSNrAdvtEVcmLZ",
    "part_id": "key_a",
    "status": "pending",
    "performed": 1,
    "required": 2,
    "report": null
  }
}

When the second keyholder performed key_b 44ms later, that response came back "status": "completed" with a report: "ritual completed with 2 of 2 parts within 44ms spread".

What perform rejects

Each of these was reproduced live:

  • Not your part - someone else's part is 403 insufficient_permissions, and the performance is not recorded.
  • Duplicate - a part already performed here is 409 part_already_performed.
  • Out of order - against a declared ordering, an early part is 400 ordering_violated.
  • Concluded - a completed or timed-out attempt is 409 not_actionable.
  • Wrong mode - perform on an inverse ritual and checkin on a forward one are both 400 mode_mismatch.

Near-miss reports

When the arm TTL expires before quorum, the background scheduler (a few seconds' sweep granularity) finalizes the attempt as timed_out and writes a report saying exactly who was missing:

{
  "performed": 1,
  "required": 2,
  "missing_parts": ["plate_b"],
  "spread_ms": 0,
  "verdict": "1 of 2 parts performed; plate_a was last at 1787926607161ms (spread 0ms)"
}

history pages through past attempts (--status completed filters), and analytics aggregates recent terminal attempts into a success rate, median spread, and the parts most often responsible for near-misses:

snug ritual history --def-id KBWergseuNgABVcZUDjr --status timed_out
snug ritual analytics --def-id KBWergseuNgABVcZUDjr --sample-size 200

Completion hooks

A definition's on_complete block fans completion out as events: pubsub_channel publishes {"attempt_id": "...", "event": "completed", "ritual_def_id": "..."} to that channel, and webhook_ids and timeline_stream dispatch to those services. Rehearsal attempts complete without publishing anything - both behaviors verified live.

Dead-man switches

Inverse mode - rituals that fire when someone does not act, with check-ins and escalation ladders - has its own page: Dead-man switches.

Limits and configuration

Up to 64 parts per ritual, windows up to 24 hours, arm TTLs up to 24 hours, and 16 escalation steps by default - all tunable, along with the scheduler sweep interval, via the RITUAL_* variables in the Ritual CONFIG reference.

Reference

On this page