# Milestone: Local-first Multiplayer Kanban
A board with drag-and-drop cards, 60fps pointer-follow, optimistic updates,
offline queue, conflict reconciliation, live presence, and SSR-first rendering —
all without React/Vue/VDOM, in a single typed Rust codebase.
This is an explicitly advanced/low-level north-star boundary sketch for hemx + hemplate + hemx-sync, not the beginner-facing authoring path. Raw sync/effect/wire vocabulary below is excluded from beginner-facing examples by design. req: milestone/001 req: milestone/002 req: milestone/003
---
## 1. Template: `board.heml`
```html
{+ self.title +}
{+ column.title +}
{+ card.title +}
{+ card.assignee +}
```
Notes on keyed scopes:
- `h-for="column in &self.columns" h-key="column.id"` — **required** for hemx-addressable nodes inside
- `h-for="card in &column.cards" h-key="card.id"` — **required**
- A slot inside a keyed loop is addressed as a generated keyed resource, never by selector strings or positional DOM targeting
- Without `h-key`, hemx rejects the build — no runtime selector fallback
---
## 2. What hemplate exports
hemplate does **not** interpret `data-hemx-*`. It records raw facts:
```rust
Node {
id: NodeId(12),
element: "article",
attrs: [
("class", "card"),
("data-hemx-slot", "card"),
("data-hemx-handle", "drag_card"),
("data-card-id", "{card.id}"),
("draggable", "true"),
],
scope: ScopeId(For { binding: "card", key_expr: "card.id" }),
}
FormSurface {
handle_attr: Some("create_card"),
controls: [
Control { name: "title", kind: Text, required: true },
Control { name: "column", kind: Select, required: true },
],
}
```
hemx reads this from hemplate Surface facts and generates scoped typed resources:
```rust
use ui::board::{forms, targets};
targets::card.replace(card.id, &CardView::from(card));
forms::create_card.clear("title");
ui::page(&BoardView::from(board));
```
No string desync. No manual ids. The generated module owns the names.
---
## 3. App State
```rust
#[hemx::app]
pub struct BoardApp {
pub board: Atom,
pub drag: Atom