Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fulsomenko/kanban/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Kanban CLI uses a JSON-based persistence layer with crash-safe atomic writes, automatic versioning, and migration support. All data is stored in a single human-readable JSON file.

File Format

Current Format (V2)

The V2 format wraps the data payload in an envelope with metadata:
{
  "version": 2,
  "metadata": {
    "instance_id": "550e8400-e29b-41d4-a716-446655440000",
    "saved_at": "2026-03-05T10:30:00Z"
  },
  "data": {
    "boards": [...],
    "columns": [...],
    "cards": [...],
    "archived_cards": [...],
    "sprints": [...],
    "dependency_graph": {...}
  }
}
Metadata Fields:
  • instance_id: UUID identifying the application instance that wrote this file
  • saved_at: ISO 8601 timestamp of when the file was saved
The instance_id is used for conflict detection when multiple instances edit the same file.

Legacy Format (V1)

The V1 format stored data at the root level without versioning:
{
  "boards": [...],
  "columns": [...],
  "cards": [...],
  "sprints": [...]
}
V1 files are automatically migrated to V2 on first load with backup creation.

Atomic Writes

Kanban uses a write-to-temp-file → atomic-rename pattern to prevent data corruption:
atomic_writer.rs
pub async fn write_atomic(path: &Path, data: &[u8]) -> KanbanResult<()> {
    // Create temp file in same directory to ensure same filesystem
    let parent = path.parent().unwrap_or_else(|| Path::new("."));
    let temp_file = tempfile::NamedTempFile::new_in(parent)?;
    let temp_path = temp_file.path().to_path_buf();

    // Write to temp file
    tokio::fs::write(&temp_path, data).await?;

    // Atomic rename (atomic on POSIX systems)
    fs::rename(&temp_path, path).await?;

    Ok(())
}
Why Atomic Writes?
  1. Crash Safety: If the process crashes mid-write, the original file remains intact
  2. No Partial Writes: The file is either fully written or unchanged
  3. Multi-Instance Safety: Works with file watchers to detect concurrent changes
Atomic renames are guaranteed on POSIX systems (Linux, macOS). On Windows, the operation is best-effort but still safer than direct writes.

File Format Versioning

Version Detection

Kanban automatically detects the file format version:
migrator.rs
pub async fn detect_version(path: &Path) -> KanbanResult<FormatVersion> {
    if !path.exists() {
        return Ok(FormatVersion::V2); // Default to V2 for new files
    }

    let content = tokio::fs::read_to_string(path).await?;
    let value: Value = serde_json::from_str(&content)?;

    // V2 files have a "version" field at root level
    if let Some(version) = value.get("version").and_then(|v| v.as_u64()) {
        return Ok(FormatVersion::from_u32(version as u32).unwrap_or(FormatVersion::V2));
    }

    // V1 files have "boards" at root level but no version field
    if value.get("boards").is_some() {
        return Ok(FormatVersion::V1);
    }

    Ok(FormatVersion::V2)
}

Migration: V1 → V2

The migration process is automatic and includes safety features:
1

Detect V1 Format

On load, check if the file is in V1 format (no version field)
2

Create Backup

Copy the original file to {filename}.v1.backup
# Example
kanban.json kanban.v1.backup
3

Transform Data

Wrap V1 data in V2 envelope structure with metadata
4

Write V2 File

Save the migrated data to the original path
5

Verify Migration

Compare migrated data with original to ensure no data loss
6

Remove Backup

If verification succeeds, delete the backup file
Migration Code:
migrator.rs
async fn migrate_v1_to_v2(path: &Path) -> KanbanResult<()> {
    // Read V1 file
    let content = tokio::fs::read_to_string(path).await?;
    let v1_data: Value = serde_json::from_str(&content)?;

    // Create backup
    let backup_path = path.with_extension("v1.backup");
    tokio::fs::copy(path, &backup_path).await?;
    tracing::info!("Created backup at {}", backup_path.display());

    // Transform to V2 format
    let v2_envelope = JsonEnvelope::new(v1_data.clone());

    // Write V2 file
    let json_str = v2_envelope.to_json_string()?;
    tokio::fs::write(path, json_str).await?;

    // Verify migration
    verify_migration(path, &v1_data).await?;

    // Remove backup on success
    tokio::fs::remove_file(&backup_path).await?;
    tracing::info!("Migration verified, backup removed");

    Ok(())
}
From the CHANGELOG (v0.1.15):
Automatic Migration: V1 data files are automatically upgraded to V2 format on load with backup creation

Backup Creation

Backups are created in two scenarios:

1. Format Migration

When migrating from V1 to V2:
  • Backup path: {original_filename}.v1.backup
  • Automatically removed after successful verification
  • Preserved if migration fails
# Before migration
kanban.json

# During migration
kanban.json
kanban.v1.backup Created automatically

# After successful migration
kanban.json Now in V2 format
# kanban.v1.backup removed

2. Manual Backup

You can manually backup your board file:
cp kanban.json kanban.backup.json
Or use the export command:
kanban export --output backup-$(date +%Y%m%d).json

Immediate Saving

Kanban uses immediate saving - changes are persisted after each action:
// All mutations flow through commands
pub trait Command: std::fmt::Debug + Send + Sync {
    fn execute(&self, snapshot: &mut Snapshot) -> KanbanResult<()>;
}
Benefits:
  • No data loss on crashes
  • Simple recovery model
  • Consistent state between instances
Trade-offs:
  • Slightly higher I/O overhead
  • Mitigated by debouncing (saves are delayed 100ms)

Bounded Save Queue

To prevent memory exhaustion during rapid changes:
Queue Configuration:
  • Maximum pending snapshots: 100
  • Saves are debounced by 100ms
  • Older saves are dropped if queue is full
From the README:
Bounded Save Queue: Maintains a queue of up to 100 pending snapshots

Storage Implementation

The persistence layer is in crates/kanban-persistence/:
kanban-persistence/
├── store/
│   ├── atomic_writer.rs    # Atomic write operations
│   └── json_file_store.rs  # JSON persistence implementation
├── migration/
│   ├── migrator.rs          # Version detection & migration
│   └── v1_to_v2.rs          # V1 → V2 migration logic
├── conflict/
│   └── detector.rs          # File metadata for conflict detection
└── watch/
    └── file_watcher.rs      # Real-time file monitoring

PersistenceStore Trait

traits.rs
#[async_trait::async_trait]
pub trait PersistenceStore {
    async fn save(&self, snapshot: StoreSnapshot) -> KanbanResult<PersistenceMetadata>;
    async fn load(&self) -> KanbanResult<(StoreSnapshot, PersistenceMetadata)>;
    async fn exists(&self) -> bool;
    fn path(&self) -> &Path;
}

JsonFileStore Implementation

json_file_store.rs
pub struct JsonFileStore {
    path: PathBuf,
    instance_id: Uuid,
    last_known_metadata: Mutex<Option<FileMetadata>>,
}

impl JsonFileStore {
    pub fn new(path: impl AsRef<Path>) -> Self {
        Self {
            path: path.as_ref().to_path_buf(),
            instance_id: Uuid::new_v4(),
            last_known_metadata: Mutex::new(None),
        }
    }
}

Rich Metadata

Cards and boards include comprehensive metadata: Card Metadata:
  • created_at - ISO 8601 timestamp
  • updated_at - ISO 8601 timestamp
  • priority - "low", "medium", "high"
  • story_points - 1-5 point estimates
  • due_date - Optional ISO 8601 date
  • sprint_logs - History of sprint assignments
  • dependencies - Parent/child relationships
Board Metadata:
  • created_at - Creation timestamp
  • updated_at - Last modification timestamp
  • sprint_prefix - Default sprint branch prefix
  • card_prefix - Default card prefix

Multi-Instance Support

Learn how multiple instances coordinate

Configuration

Configure file paths and environment variables

Build docs developers (and LLMs) love