Boundless

A DCB-inspired Event Store for TypeScript

TypeScript better-sqlite3 sql.js (Browser)
🎮 Try Live Demo View on GitHub Documentation
🚫

No Streams

Events organized via configurable consistency keys, not rigid stream boundaries.

⚙️

Config-based Keys

Extract consistency keys from event payloads. Events stay pure business data.

🔐

HMAC Tokens

Tamper-proof consistency tokens with HMAC-SHA256 signatures.

Conflict Detection

Get exactly what changed since your read — with the new token for retry.

🔄

Auto-Reindex

Change your consistency config, restart, keys are automatically rebuilt.

💾

SQLite & Memory

Production-ready SQLite storage or in-memory for testing.

Beyond Traditional DCB

Dynamic Consistency Boundaries (DCB) solves optimistic concurrency by attaching consistency keys (tags) to events. Boundless takes this further:

Traditional DCB
Keys (tags) are written onto events at append time.
Once written, they're immutable.
Boundless Approach
Keys are extracted from payloads via config.
Events stay pure. Config can change anytime.
💡 Why more flexible?

No migration needed — Change your consistency boundaries by updating config, not events
Events stay clean — Business data only, no infrastructure concerns
Retroactive changes — Add new keys to existing events via reindex
DRY — Key paths defined once in config, not repeated on every append

The DCB Pattern: Read → Decide → Write

// 1️⃣ READ — Query events by consistency keys
const { events, token } = await store.read({
  conditions: [
    { type: 'CourseCreated', key: 'course', value: 'cs101' },
    { type: 'StudentSubscribed', key: 'course', value: 'cs101' },
  ]
});
// token captures: "I read all events matching these conditions up to position X"

// 2️⃣ DECIDE — Project state and check business rules
const course = events.find(e => e.type === 'CourseCreated');
const enrolled = events.filter(e => e.type === 'StudentSubscribed').length;

if (enrolled >= course.data.capacity) {
  throw new Error('Course is full!');
}

// 3️⃣ WRITE — Append with the token from your read
const result = await store.append([
  { type: 'StudentSubscribed', data: { courseId: 'cs101', studentId: 'alice' } }
], token);  // ← Token ensures no one else wrote since your read!

Conflict? No Problem!

if (result.conflict) {
  // Someone else enrolled while you were deciding!
  console.log('Events since your read:', result.conflictingEvents);
  
  // You get a fresh token to retry
  const freshToken = result.newToken;
  
  // Re-read, re-decide, re-write...
} else {
  // Success!
  console.log('Enrolled at position', result.position);
  console.log('New token for next operation:', result.token);
}

How Conflict Detection Works

// The token captures your exact query scope:
token = {
  q: [{ type: 'StudentSubscribed', key: 'course', value: 'cs101' }],
  pos: 5  // Position at time of read
}

// On append, Boundless checks:
// "Are there NEW events (pos > 5) that MATCH these conditions?"

// ✅ NO conflict if someone wrote:
//    - StudentSubscribed for course='math201' (different key value)
//    - CourseCreated for cs101 (different event type)

// ❌ CONFLICT only if:
//    - StudentSubscribed for course='cs101' was written
//    - (matches your query conditions!)
💡 Conflicts are scoped to your read — not global. This is the power of Dynamic Consistency Boundaries!

Query Across Multiple Dimensions

// Traditional streams: ONE boundary (e.g., per course)
// DCB: Query ANY combination of keys!

// "Has Alice already enrolled in CS101?"
const { events, token } = await store.read({
  conditions: [
    { type: 'StudentSubscribed', key: 'course', value: 'cs101' },
    { type: 'StudentSubscribed', key: 'student', value: 'alice' },
  ]
});
// Checks BOTH course boundary AND student boundary in one query!

Config-based Key Extraction

// Keys are extracted from event payloads via configuration
// Events stay pure — no tags or metadata pollution!

const consistency = {
  eventTypes: {
    CourseCreated: {
      keys: [
        { name: 'course', path: 'data.courseId' }
      ]
    },
    StudentSubscribed: {
      keys: [
        { name: 'course', path: 'data.courseId' },
        { name: 'student', path: 'data.studentId' },
        { name: 'semester', path: 'data.semester', transform: 'UPPER' }
      ]
    }
  }
};

// When an event is appended:
// { type: 'StudentSubscribed', data: { courseId: 'cs101', studentId: 'alice', semester: 'ws24' } }
//
// Keys are automatically extracted and indexed:
//   → course: 'cs101'
//   → student: 'alice'
//   → semester: 'WS24' (transformed to uppercase)

Auto-Reindex on Config Change

// The config is hashed and stored in the database
// On startup, Boundless compares hashes:

// stored_hash: "a1b2c3..."  (from last run)
// current_hash: "x9y8z7..." (from your config)

if (stored_hash !== current_hash) {
  // 🔄 Config changed! Rebuilding key index...
  //    Old hash: a1b2c3...
  //    New hash: x9y8z7...
  
  // All events are re-scanned
  // Keys are re-extracted with new config
  // Index is rebuilt automatically
  
  // ✅ Reindex complete: 1523 events, 4211 keys (847ms)
}

// No manual migration needed!
// Just change your config and restart.

How It Works

1
Event Appended
You append an event with business data
{ type: 'StudentSubscribed', data: { courseId: 'cs101', studentId: 'alice' } }
2
Keys Extracted
Config tells Boundless which fields are consistency keys
course → 'cs101', student → 'alice'
3
Index Updated
Keys are stored in a separate index table, linked to the event position
event_keys: [pos:1, course, cs101], [pos:1, student, alice]
4
Query by Keys
Find all events matching any combination of key conditions
WHERE (type='StudentSubscribed' AND key='course' AND value='cs101')
💡 Config changed? Boundless compares the config hash on startup. If different, all keys are automatically re-extracted from existing events. No manual migration needed!