Branching & Merging
Git-style content workflows with copy-on-write branching
Overview
Helix brings Git-style branching to content management. Create cheap branches for parallel workflows, experiment with changes, and merge back using CRDT-based conflict-free algorithms.
Branching enables powerful workflows: draft content on a feature branch, prepare seasonal campaigns, test content changes, coordinate translations, or let multiple teams work in parallel without conflicts.
Branch Architecture
Branches form a Directed Acyclic Graph (DAG) with parent-child relationships. Every Space starts with a main branch:
main ├── feature/new-homepage ├── campaign/summer-2024 └── translations/german
Copy-on-Write (COW)
Creating a branch is an O(1) operation - no data is copied at creation time. This makes branching extremely cheap.
How COW Works
- Branch Creation: Insert a row into
branchestable with a parent pointer - Reading: If an entity doesn't exist on the current branch, fall back to the parent
- First Write: On the first modification, copy the parent's projection and apply the change
- Subsequent Writes: Modify the branch-specific projection
Creating Branches
// Get a reference to a space
const space = await client.getSpace('my-space-id');
// Create a new branch
const featureBranch = await space.createBranch('feature/new-hero');
Working with Branches
// Get content from a specific branch
const pages = await client
.select()
.from(pages)
.execute({ branch: 'feature/new-homepage', locale: 'en' });
// Create content on a branch using a branch reference
await featureBranch.save('Page', {
slug: 'new-feature',
title: 'New Feature Page'
});
Time-Travel with Time-Based Branches
Helix's event-sourced architecture enables powerful time-travel capabilities through time-based branches. These special read-only branches let you view content exactly as it existed at any point in the past.
Time-based branches are perfect for audit trails, compliance reporting, debugging content issues, and historical analysis - all without disrupting your live system.
Creating a Time-Based Branch
Create a branch that materializes content from a specific point in time:
// Create a branch showing content as it was on January 1, 2024
const timeBranch = await space.createBranch('audit-2024-01-01', {
pointInTimeAt: new Date('2024-01-01T00:00:00Z'),
materializeAsync: true, // Returns immediately, materializes in background
});
// Poll for materialization status
const status = await client.getBranchStatus('audit-2024-01-01');
console.log(status.materializationStatus); // 'materializing' | 'ready' | 'failed'
// Once ready, query the historical state
const historicalPages = await client
.select()
.from(pages)
.where(eq(pages.status, 'published'))
.execute({ branch: 'audit-2024-01-01', locale: 'en' });
How It Works
- Incremental Snapshots: The system automatically maintains daily snapshots of entity projections and edges
- Efficient Reconstruction: Helix finds the nearest snapshot before the requested date and replays events forward
- Asynchronous Materialization: Time branch creation returns immediately; materialization happens in the background
- Retention Policy: Old snapshots are pruned automatically (last 7 daily, monthly for past year, yearly indefinitely)
Use Cases
Audit & Compliance
Review content as it appeared on a specific date for regulatory compliance:
// Create audit branch for Q4 2023
const auditBranch = await space.createBranch('audit-q4-2023', {
pointInTimeAt: new Date('2023-12-31T23:59:59Z'),
});
Debugging Content Changes
Investigate when and how content changed by comparing states:
// Create branches at different points in time
const beforeIncident = await space.createBranch('before-incident', {
pointInTimeAt: new Date('2024-03-14T10:00:00Z'),
});
const afterIncident = await space.createBranch('after-incident', {
pointInTimeAt: new Date('2024-03-14T15:00:00Z'),
});
// Compare the two states to find what changed
Historical Analysis
Measure content evolution by comparing current state against historical baselines:
// Compare current content with last year
const lastYear = await space.createBranch('baseline-2023', {
pointInTimeAt: new Date('2023-01-01T00:00:00Z'),
});
Time-based branches are read-only. You cannot modify content on these branches - they exist solely for querying historical states.
Merging Branches
Merging combines changes from a source branch into a target branch using CRDT-based algorithms for conflict-free merging.
// Merge the branch back to its source (e.g., 'main') await featureBranch.mergeToSource();
CRDT Merge Rules
Scalars: Last-Write-Wins (LWW)
For scalar fields (strings, numbers, booleans), the most recent change wins based on timestamp.
Arrays: Observed-Remove Sets
Component arrays and relation sets use observed-remove sets, preserving both additions and modifications:
// Base (LCA) sections: [Hero, TextBlock] // Source branch adds CTA sections: [Hero, TextBlock, CTA] // Target branch modifies Hero sections: [Hero (modified), TextBlock] // After merge (both changes preserved) sections: [Hero (modified), TextBlock, CTA]
CRDTs guarantee deterministic, conflict-free merges. The same merge always produces the same result regardless of order!
Branch Workflows
Feature Development
- Create feature branch from main
- Make content changes on feature branch
- Review changes in preview environment
- Merge feature branch back to main
- Deploy main to production
Campaign Preparation
- Create campaign branch (e.g.,
campaign/black-friday) - Add campaign-specific content
- Schedule merge for campaign launch date
- Auto-merge at launch time
- Content goes live instantly
Translation Workflow
- Create translation branch
- Add translations for new locale
- Translators work in parallel
- Review translations
- Merge back to main
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
| Create Branch | O(1) | Just inserts into branches table |
| Read (no COW yet) | O(1) | Falls back to parent automatically |
| First Write (COW) | O(n) | Copies parent projection |
| Merge | O(m) | m = number of modified entities |
Best Practices
- Keep branches short-lived - merge frequently
- Use descriptive names:
feature/,campaign/,fix/prefixes - Delete merged branches to reduce clutter
- Use branches for isolation: teams, campaigns, experiments, translations
- Review before merging - preview changes in staging