# BoardDown
> **Local-first, file-based Kanban storage engine with sync capabilities**
>
> Think "Markdown meets Kanban" — version controlled, offline-capable, and programmable.
[](https://www.rust-lang.org)
[](https://www.typescriptlang.org)
[](https://crates.io)
## Table of Contents
- [Philosophy](#philosophy)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [The BoardDown Specification](#the-boarddown-specification)
- [Rust API](#rust-api)
- [TypeScript API](#typescript-api)
- [Storage Backends](#storage-backends)
- [Multi-File Workspaces](#multi-file-workspaces)
- [Roadmap](#roadmap)
---
## Philosophy
**Why BoardDown?**
Existing Kanban tools lock your data in proprietary cloud databases. BoardDown treats your project management data as **code**:
- **Git-compatible**: Boards are Markdown, diff them in PRs
- **Offline-first**: Local SQLite with optional cloud sync
- **Programmable**: Rust/TS APIs for custom automation
- **Human-readable**: Edit in any text editor, render in any app
**Use Cases:**
- Personal task management in `~/tasks/` synced via Git
- Team sprints with conflict-free replicated data types (CRDTs)
- Embedded Kanban in IDEs (VSCode, Neovim extensions)
- CI/CD pipeline visualization (`/.boarddown/ci.board.md`)
---
## Quick Start
### Installation
```bash
# CLI tool
cargo install boarddown-cli
# Or add to your Rust project
cargo add boarddown-core boarddown-fs
# For Node.js projects
npm install @boarddown/core
```
### The 30-Second Tutorial
Create a file `sprint-42.board.md`:
```markdown
---
id: "sprint-42"
board: "Sprint 42: Auth Refactor"
id-prefix: "TICKET"
---
## Todo
- [ ] BD-001: Implement OAuth2 PKCE flow
Tags: security, backend
Estimate: 5
- [ ] BD-002: Add session rotation
Tags: security
Depends: BD-001
## In Progress
- [~] BD-003: Refactor JWT middleware
Assign: alice@example.com
Started: 2024-01-15
## Done
- [x] BD-004: Setup test fixtures
Closed: 2024-01-14
```
Then interact with it:
```rust
use boarddown::{Workspace, Query};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open workspace (folder containing .board.md files)
let ws = Workspace::open("./sprints").await?;
// Load board
let board = ws.get_board("sprint-42").await?;
// Query tasks
let blocked = board.query()
.status(Status::Todo)
.tag("security")
.depends_on_existing(false) // Find root tasks
.execute().await?;
// Move task programmatically
let mut task = board.get_task("BD-001").await?;
task.set_status(Status::InProgress);
task.set_assignee("bob@example.com");
ws.save_task(&task).await?;
Ok(())
}
```
Or in TypeScript:
```typescript
import { Workspace, Status } from '@boarddown/core'
const ws = await Workspace.open('./sprints')
const board = await ws.getBoard('sprint-42')
// Subscribe to changes (local or remote)
board.onChange((event) => {
console.log(`Task ${event.taskId} moved to ${event.to}`)
})
// Create task programmatically
const task = await board.createTask({
title: 'Fix edge case',
column: 'Todo',
metadata: {
priority: 'High',
estimate: 3
}
})
```
---
## Architecture
```text
┌─────────────────────────────────────────────────────────────┐
│ Applications │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CLI Tool │ │ Tauri App│ │ VSCode │ │ Web App │ │
│ │ │ │ (Desktop)│ │ Extension│ │ (Next.js)│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼─────────────┼──────────────┼────────┘
│ │ │ │
└──────────────┴──────┬──────┴──────────────┘
│
┌──────────▼──────────┐
│ boarddown-napi │ ◄── Node.js bindings
│ (napi-rs) │ via napi-rs
└──────────┬──────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌───────▼───────┐ ┌──────────▼──────────┐ ┌──────▼───────┐
│ Core Types │ │ Core Engine │ │ Sync │
│ (Schema) │ │ (Parse/Query/CRDT) │ │ (CRDT/OT) │
└───────┬───────┘ └──────────┬──────────┘ └──────┬───────┘
│ │ │
└─────────────────────┼────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌───────▼───────┐ ┌──────────▼──────────┐ ┌──────▼───────┐
│ FS Backend │ │ SQLite Backend │ │ Cloud │
│ (.board.md) │ │ (high-perf cache) │ │ (optional) │
└───────────────┘ └─────────────────────┘ └──────────────┘
```
### Crate Structure (`crates/`)
| `boarddown-core` | Parser, AST, validation engine, CRDT types | `serde`, `serde_yaml`, `chrono` |
| `boarddown-fs` | Filesystem storage backend, git integration | `boarddown-core`, `tokio`, `notify` |
| `boarddown-db` | SQLite backend, indexing, full-text search | `boarddown-core`, `sqlx`, `rusqlite` |
| `boarddown-sync` | Conflict resolution, operational transforms | `boarddown-core`, `automerge` (optional) |
| `boarddown-napi` | Node.js FFI bindings | `napi`, `napi-derive`, `boarddown-core` |
| `boarddown-cli` | Command-line interface | `clap`, `boarddown-fs`, `boarddown-db` |
| `boarddown-server` | Optional WebSocket sync server | `axum`, `boarddown-sync` |
---
## The BoardDown Specification
### File Format (`.board.md`)
BoardDown files are **CommonMark compliant** with YAML frontmatter and task syntax:
```markdown
---
board: "Sprint 42"
id-prefix: "BD"
default-tags: ["sprint-42"]
storage: sqlite
autosync: true
---
# Sprint 42: Auth Refactor
## Meta
- Start: 2024-01-01
- End: 2024-01-15
- Velocity: 34
## Columns
### Todo
- [ ] {BD-001} Implement OAuth2 PKCE
Description: |
We need to support PKCE for mobile apps.
See RFC 7636 for details.
Tags: security, oauth
Estimate: 5
Epic: Auth-2024
- [ ] {BD-002} Session rotation
Tags: security
Depends: {BD-001}
Due: 2024-01-10
### In Progress
- [~] {BD-003} JWT middleware refactor
Assign: alice@example.com
Branch: feature/jwt-refactor
Started: 2024-01-08
### Done
- [x] {BD-004} Test fixtures
Closed: 2024-01-05
PR: https://github.com/org/repo/pull/123
```
### Syntax Reference
**Task Status:**
| Syntax | Status | Emoji |
|--------|--------|-------|
| `- [ ]` | Todo | ⬜ |
| `- [~]` | In Progress | 🔄 |
| `- [>]` | Ready/Review | 👀 |
| `- [x]` | Done | ✅ |
| `- [?]` | Blocked/Icebox | 🧊 |
| `- [!]` | Urgent | 🔥 |
**Task IDs:**
- `{BD-001}` → Auto-linking, unique per workspace
- Auto-generated if omitted: `{board-prefix}-{timestamp}`
**Metadata (YAML inline):**
```yaml
# Standard fields
Assign: string | string[] # Email or username
Tags: string[] # Categorization
Priority: Low | Medium | High | Critical
Estimate: number # Story points or hours
Due: ISO-8601 date
Started: ISO-8601 datetime
Closed: ISO-8601 datetime
# Extensible custom fields
Epic: string # Grouping
Branch: string # Git association
PR: url # Pull request link
Refs: id[] # Related (non-blocking)
Depends: id[] # Blocking dependencies
```
### Multi-File Workspaces
For large projects, split boards across files:
```
my-project/
├── .boarddown/ # Workspace config
│ ├── config.toml # Global settings
│ ├── templates/ # Task templates
│ └── hooks/ # Git hooks
├── product/
│ ├── roadmap.board.md # High-level roadmap
│ └── backlog.board.md # Unsorted ideas
├── sprints/
│ ├── sprint-42.board.md # Current sprint
│ └── sprint-43.board.md # Next sprint
├── teams/
│ ├── backend.board.md # Team-specific board
│ └── frontend.board.md
└── archive/
└── 2023/ # Completed boards
```
**Workspace Config** (`.boarddown/config.toml`):
```toml
[workspace]
name = "My Project"
id-scheme = "TICKET-{board}-{seq}"
default-storage = "sqlite"
[storage.sqlite]
path = ".boarddown/cache.db"
enable-ft-index = true # Full-text search
[storage.filesystem]
watch = true # Auto-reload on file changes
git-auto-commit = false # Commit changes automatically
[sync]
enabled = true
provider = "crdt" # "crdt", "git", or "custom"
conflict-resolution = "last-write-wins"
[hooks]
on-task-move = "scripts/notify.sh"
on-board-save = "git add ."
# Schema validation
[schema.custom-fields]
"risk-level" = { type = "enum", values = ["low", "medium", "high"] }
"story-points" = { type = "number", min = 0, max = 100 }
```
---
## Rust API
### Core Types
```rust
use boarddown_core::{
Workspace, Board, Task, TaskId, Column, Status,
Metadata, Dependency, QueryBuilder
};
use boarddown_fs::FilesystemStorage;
use boarddown_db::SqliteStorage;
// Initialize storage backend
let storage = SqliteStorage::new(".boarddown/cache.db").await?;
// Open workspace
let mut ws = Workspace::builder()
.path("./project")
.storage(storage)
.build()
.await?;
// Access board
let board = ws.board("sprints/sprint-42").await?;
// Type-safe task creation
let task = Task::builder()
.id(TaskId::new("sprint-42", 1))
.title("Implement feature")
.status(Status::Todo)
.column("Todo")
.metadata(Metadata::new()
.with("assign", "alice@example.com")
.with("estimate", 5)
.with("tags", vec!["backend"]))
.depends_on(TaskId::parse("BD-001")?)
.build();
// Save with conflict detection
ws.save_task(&task).await?; // Returns Err(Conflict) if modified elsewhere
// Complex queries
let results = board.query()
.status_in([Status::Todo, Status::InProgress])
.metadata("assign", "alice@example.com")
.due_before(chrono::Utc::now() + Duration::days(7))
.limit(10)
.execute()
.await?;
// Transactions
task.set_status(Status::Done);
txn.save_task(&task).await?;
Ok(())
}).await?;
```
### Event System
```rust
// Subscribe to changes (cross-process safe)
let mut rx = ws.subscribe();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
match event {
BoardEvent::TaskCreated { board_id, task_id } => {
println!("New task in {}: {}", board_id, task_id);
}
BoardEvent::TaskMoved { task_id, from, to } => {
if to == Status::Done {
trigger_ci_pipeline(&task_id).await;
}
}
BoardEvent::DependencyResolved { task_id, dependency } => {
notify_assignee(&task_id, "Unblocked!").await;
}
_ => {}
}
}
});
```
### Storage Traits
Implement custom backends:
```rust
use boarddown_core::storage::{Storage, Transaction};
pub struct RedisStorage { /* ... */ }
#[async_trait]
impl Storage for RedisStorage {
async fn load_board(&self, id: &BoardId) -> Result<Board, Error>;
async fn save_task(&self, task: &Task) -> Result<(), Error>;
async fn query(&self, q: Query) -> Result<Vec<Task>, Error>;
// Sync support
async fn get_changeset(&self, since: Version) -> Result<Changeset, Error>;
async fn apply_changeset(&self, cs: Changeset) -> Result<(), Error>;
}
```
---
## TypeScript API
Via `boarddown-napi` (Node.js native addon):
```typescript
import {
Workspace,
Board,
Status,
Priority,
QueryBuilder
} from '@boarddown/core'
// Async initialization
const ws = await Workspace.open('./project', {
storage: 'sqlite',
watch: true // Hot-reload on file changes
})
// Type-safe board access
const board: Board = await ws.getBoard('sprint-42')
// Reactive queries (auto-update on changes)
const tasks = board.query({
status: [Status.Todo, Status.InProgress],
metadata: {
priority: Priority.High,
assign: 'alice@example.com'
},
orderBy: 'due',
limit: 20
}).subscribe((tasks) => {
console.log('Tasks updated:', tasks.length)
})
// Task manipulation
const task = await board.createTask({
title: 'New feature',
column: 'Todo',
status: Status.Todo,
metadata: {
estimate: 5,
tags: ['backend', 'urgent']
},
dependencies: ['BD-001'] // Auto-validates existence
})
// Move with business logic hooks
await board.moveTask(task.id, 'In Progress', {
validate: (t) => t.dependencies.every(d => d.isResolved()),
onSuccess: (t) => console.log(`Started ${t.id}`)
})
// Sync across devices
const sync = ws.enableSync({
provider: 'crdt',
url: 'wss://sync.boarddown.dev',
auth: process.env.BOARDDOWN_TOKEN
})
sync.on('conflict', (conflict) => {
console.log('Merge conflict:', conflict.resolve('local')) // or 'remote', 'manual'
})
```
### React/Vue Integration
```typescript
// React hook
import { useBoard, useTasks } from '@boarddown/react'
function SprintBoard({ boardId }: { boardId: string }) {
const { board, loading, error } = useBoard(boardId)
const { tasks, moveTask } = useTasks(boardId, {
status: Status.InProgress
})
if (loading) return <Spinner />
return (
<Kanban
columns={board.columns}
onCardMove={(cardId, dest) => moveTask(cardId, dest)}
/>
)
}
```
---
## Storage Backends
### Filesystem (Git-Optimized)
- One file per board
- Line-oriented diff for tasks
- Frontmatter for metadata
- Attachments in `.boarddown/attachments/{board-id}/`
### SQLite (High Performance)
- Indexed queries (< 1ms for 10k tasks)
- Full-text search (SQLite FTS5)
- JSON metadata column for schemaless fields
- WAL mode for concurrent access
### Hybrid Mode (Recommended)
```toml
[storage]
type = "hybrid"
cache = "sqlite"
source-of-truth = "filesystem"
sync-interval = "30s" # Auto-sync FS <-> SQLite
```
---
## Roadmap
### Phase 1: Core (v0.1.0) - Current
- [x] Parser (markdown to AST)
- [x] FS storage backend
- [x] SQLite backend with FTS5
- [x] Basic TypeScript bindings (napi-rs)
- [x] Hybrid storage mode
- [x] Full-text search (FTS5)
- [x] Custom fields validation
- [x] Hook execution system
- [x] CLI with init, add, list, move commands
- [x] WebSocket sync server
- [x] CRDT conflict resolution
### Phase 2: Ecosystem (v0.2.0)
- [ ] React integration hooks
- [ ] Vue integration
- [ ] VSCode extension
- [ ] CLI TUI (ratatui)
- [ ] GitHub Actions integration
### Phase 3: Enterprise (v0.3.0)
- [ ] RBAC (Role-based access control)
- [ ] Audit logging
- [ ] Attachment support
- [ ] Template system
---
## Contributing
We welcome contributions! See [CONTRIBUTING.md](./CONTRIBUTING.md).
## License
MIT