Kanban API Service — Architecture Design

Summary

Overview

The Kanban API service moves Forge's task management from a flat markdown file (Task Board.md) to a structured Supabase-backed system with a full REST API. This makes the dashboard fully bidirectional — tasks can be created, moved, updated, completed, and deleted from the GUI without string-munging a markdown file over HTTP. The service runs on the VPS as a standalone Express/TypeScript process on port 5016, follows Forge service conventions (/health, /docs, /v1/*), and integrates with Ralph's existing ralph_queue table so tasks can flow seamlessly from the human kanban board into the autonomous agent pipeline. Task Board.md becomes a generated export — still committed by the guardian, still readable by Claude sessions — but no longer the source of truth.

Architecture

                              +------------------+
                              |   Vercel App     |
                              |   (Dashboard)    |
                              +--------+---------+
                                       |
                             HTTPS (Vercel proxy)
                                       |
                                       v
+------------------+          +------------------+          +------------------+
|  Telegram Bot    |   HTTP   |   KANBAN API     |  SQL     |    Supabase      |
|  (Commander)     +-------->+   :5016           +-------->+  kanban_tasks    |
+------------------+          |                  |          |  kanban_columns  |
                              |  /v1/tasks       |          |  kanban_history  |
+------------------+   HTTP   |  /v1/columns     |          |  ralph_queue     |
|  Claude Sessions +-------->+  /v1/dispatch    |          +------------------+
|  (rituals, CLI)  |          |  /v1/export      |
+------------------+          |  /v1/import      |
                              +--------+---------+
                                       |
                              +--------v---------+
                              |  Task Board.md   |
                              |  (generated       |
                              |   export, cron)   |
                              +------------------+
Data Flow
  1. Dashboard / Bot / CLI → POST /v1/tasks → Supabase kanban_tasks
  2. Dashboard drag-drop → PATCH /v1/tasks/:id → updates column/position
  3. "Send to Ralph" → POST /v1/dispatch → inserts into ralph_queue
  4. Cron (60s) → GET /v1/export → writes Task Board.md
  5. One-time → POST /v1/import → parses Task Board.md → seeds kanban_tasks

Components

1. Kanban API Server

Endpoints

Method Path Description
GET /health Service health + task count + last sync time
GET /docs Returns CLAUDE.md as plain text
GET /v1/tasks List tasks with filters (column, project, priority, status)
GET /v1/tasks/:id Get single task with history
POST /v1/tasks Create a task
PATCH /v1/tasks/:id Update task fields (title, column, priority, project, position)
DELETE /v1/tasks/:id Soft-delete a task (sets deleted_at)
POST /v1/tasks/:id/done Mark task complete (moves to Done, sets completed_at)
POST /v1/tasks/:id/reopen Reopen a completed task
POST /v1/tasks/reorder Batch update positions within a column
GET /v1/columns List all columns with task counts
POST /v1/dispatch Send a task to ralph_queue for autonomous execution
GET /v1/export Generate Task Board.md from current state
POST /v1/export/write Generate and write Task Board.md to disk
POST /v1/import Parse Task Board.md and seed database (one-time migration)
GET /v1/stats Task counts by column, project, priority; completion rate; velocity

Data Model

Three tables in Supabase with RLS policies, views, and triggers:

kanban_columns

CREATE TABLE IF NOT EXISTS kanban_columns (
  id          TEXT PRIMARY KEY,         -- 'today', 'soon', 'later', 'waiting', 'inbox', 'done', 'agenda'
  label       TEXT NOT NULL,            -- 'Today', 'Soon', 'Later', etc.
  position    INT NOT NULL DEFAULT 0,   -- display order
  description TEXT,                     -- section description
  color       TEXT,                     -- hex color for UI
  is_terminal BOOLEAN DEFAULT FALSE,    -- 'done' column = terminal
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Seed default columns
INSERT INTO kanban_columns (id, label, position, description, is_terminal) VALUES
  ('today',   'Today',   0, 'Active focus for today. Max 3-5 items.', FALSE),
  ('soon',    'Soon',    1, 'Next 2-3 days.', FALSE),
  ('later',   'Later',   2, 'Backlog, no urgency.', FALSE),
  ('waiting', 'Waiting', 3, 'Blocked on someone/something external.', FALSE),
  ('inbox',   'Inbox',   4, 'Uncategorized captures. Triaged during /start.', FALSE),
  ('done',    'Done',    5, 'Completed tasks with dates. Newest at top.', TRUE)
ON CONFLICT (id) DO NOTHING;

kanban_tasks

CREATE TABLE IF NOT EXISTS kanban_tasks (
  id            BIGSERIAL PRIMARY KEY,
  title         TEXT NOT NULL,
  column_id     TEXT NOT NULL REFERENCES kanban_columns(id) DEFAULT 'inbox',
  position      INT NOT NULL DEFAULT 0,
  priority      TEXT NOT NULL DEFAULT 'normal'
                CHECK (priority IN ('urgent', 'medium', 'normal')),
  project       TEXT,                              -- 'FORGE', 'HC', 'MASTERY', etc.
  tags          TEXT[] DEFAULT '{}',
  description   TEXT,
  due_date      DATE,
  assignee      TEXT,                              -- 'jason', 'ralph', 'forge'
  source        TEXT DEFAULT 'manual',             -- 'manual', 'import', 'telegram', etc.
  agenda_person TEXT,                              -- for agenda items
  agenda_topic  TEXT,                              -- for agenda items
  completed_at  TIMESTAMPTZ,
  deleted_at    TIMESTAMPTZ,                       -- soft delete
  ralph_queue_id BIGINT REFERENCES ralph_queue(id),
  metadata      JSONB DEFAULT '{}',
  created_at    TIMESTAMPTZ DEFAULT NOW(),
  updated_at    TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes for common query patterns
CREATE INDEX idx_kanban_tasks_column   ON kanban_tasks(column_id, position);
CREATE INDEX idx_kanban_tasks_project  ON kanban_tasks(project);
CREATE INDEX idx_kanban_tasks_priority ON kanban_tasks(priority);
CREATE INDEX idx_kanban_tasks_active   ON kanban_tasks(column_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_kanban_tasks_done     ON kanban_tasks(completed_at DESC) WHERE column_id = 'done';

kanban_history

CREATE TABLE IF NOT EXISTS kanban_history (
  id          BIGSERIAL PRIMARY KEY,
  task_id     BIGINT NOT NULL REFERENCES kanban_tasks(id) ON DELETE CASCADE,
  action      TEXT NOT NULL CHECK (action IN (
                'created', 'updated', 'moved', 'completed', 'reopened',
                'deleted', 'dispatched', 'imported'
              )),
  from_column TEXT,
  to_column   TEXT,
  changes     JSONB DEFAULT '{}',      -- field-level diff
  actor       TEXT DEFAULT 'system',   -- 'jason', 'dashboard', 'telegram', 'ralph'
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_kanban_history_task ON kanban_history(task_id, created_at DESC);
CREATE INDEX idx_kanban_history_time ON kanban_history(created_at DESC);

Views

-- Active board: non-deleted tasks ordered by column + position
CREATE OR REPLACE VIEW v_kanban_board AS
SELECT t.id, t.title, t.column_id, t.position, t.priority,
  t.project, t.tags, t.description, t.due_date, t.assignee,
  t.agenda_person, t.agenda_topic, t.completed_at,
  t.ralph_queue_id, t.source, t.metadata, t.created_at, t.updated_at,
  c.label AS column_label, c.position AS column_position
FROM kanban_tasks t
JOIN kanban_columns c ON c.id = t.column_id
WHERE t.deleted_at IS NULL
ORDER BY c.position, t.position, t.created_at DESC;

-- Column summary with counts
CREATE OR REPLACE VIEW v_kanban_summary AS
SELECT c.id AS column_id, c.label, c.position,
  COUNT(t.id) FILTER (WHERE t.deleted_at IS NULL) AS task_count,
  COUNT(t.id) FILTER (WHERE t.deleted_at IS NULL AND t.priority = 'urgent') AS urgent_count
FROM kanban_columns c
LEFT JOIN kanban_tasks t ON t.column_id = c.id
GROUP BY c.id, c.label, c.position
ORDER BY c.position;

-- Daily completion velocity (last 30 days)
CREATE OR REPLACE VIEW v_kanban_velocity AS
SELECT DATE(completed_at) AS day, COUNT(*) AS completed
FROM kanban_tasks
WHERE completed_at IS NOT NULL
  AND completed_at > NOW() - INTERVAL '30 days'
  AND deleted_at IS NULL
GROUP BY DATE(completed_at)
ORDER BY day DESC;

-- Eisenhower matrix view
CREATE OR REPLACE VIEW v_kanban_eisenhower AS
SELECT t.id, t.title, t.priority, t.project, t.tags, t.column_id,
  CASE
    WHEN t.column_id = 'today' AND t.priority = 'urgent' THEN 'q1'
    WHEN t.column_id = 'today' THEN 'q2'
    WHEN t.column_id = 'soon' AND t.priority = 'urgent' THEN 'q1'
    WHEN t.column_id = 'soon' AND t.priority = 'medium' THEN 'q2'
    WHEN t.column_id = 'soon' THEN 'q3'
    ELSE 'q4'
  END AS quadrant
FROM kanban_tasks t
WHERE t.deleted_at IS NULL AND t.completed_at IS NULL
  AND t.column_id NOT IN ('done', 'waiting', 'inbox')
ORDER BY t.position;

RLS Policies

Security Model

Anon key: Read-only access (dashboard reads).
Service role: Full access (API server uses service key for all writes). Same pattern as ralph_queue.

Integration Points

Dashboard (Vercel App)

Telegram Bot (Commander)

Ralph (Autonomous Agent)

Dispatch Flow

Dashboard "Send to Ralph" button calls POST /v1/dispatch which: (1) Creates a ralph_queue row with task text, project, priority mapping. (2) Updates kanban_tasks.ralph_queue_id to link them. (3) Moves the kanban task to waiting column with tag (ralph-queued). When Ralph completes the task, a callback marks it done with actor: 'ralph'.

Priority mapping: Kanban urgent → ralph_queue priority 20, medium → 50, normal → 70

Rituals (/start, /sync, /wrap-up)

Memory & Context

Service Convention Compliance

File Structure

services/kanban-api/
  CLAUDE.md              # Service docs (served at GET /docs)
  package.json           # Dependencies: express, @supabase/supabase-js
  tsconfig.json
  src/
    server.ts            # Express app, routes, health, docs
    routes/
      tasks.ts           # CRUD for /v1/tasks
      columns.ts         # /v1/columns
      dispatch.ts        # /v1/dispatch (Ralph integration)
      export.ts          # /v1/export, /v1/import
      stats.ts           # /v1/stats
    lib/
      supabase.ts        # Supabase client (service-role)
      exporter.ts        # Generate Task Board.md from DB
      importer.ts        # Parse Task Board.md into DB rows
      history.ts         # Write kanban_history entries
    cron/
      export-sync.ts     # setInterval that writes Task Board.md
  scripts/
    migrate.ts           # One-time migration CLI
    setup.sql            # SQL from Data Model section
  forge-kanban-api.service  # systemd unit file

Build Phases

Phase 1 API + Schema ~30 min

  1. Create services/kanban-api/ directory structure
  2. Implement Express server with /health, /docs
  3. Run SQL schema in Supabase
  4. Implement GET /v1/tasks and POST /v1/tasks
  5. Register in config/services.json
  6. Create systemd unit file

Phase 2 Full CRUD + Export ~20 min

  1. Implement PATCH /v1/tasks/:id, DELETE, /done, /reopen
  2. Implement POST /v1/tasks/reorder
  3. Implement GET /v1/export (markdown generator)
  4. Implement export cron (60s interval)

Phase 3 Import + Migration ~15 min

  1. Implement POST /v1/import (parse Task Board.md)
  2. Build migration script
  3. Run one-time migration of existing tasks
  4. Verify export matches original format

Phase 4 Dispatch + Ralph Integration ~15 min

  1. Implement POST /v1/dispatch (kanban → ralph_queue)
  2. Add Ralph completion webhook/poll
  3. Test full flow: create task → dispatch → Ralph completes → task marked done

Phase 5 Dashboard Integration ~30 min

  1. Create lib/kanban-client.ts in dashboard app
  2. Update Today tab to read from API
  3. Update Tasks tab to use API CRUD
  4. Add drag-and-drop column moves
  5. Add "Send to Ralph" button
  6. Deploy to Vercel

Phase 6 Telegram + Rituals ~15 min

  1. Add /task, /done, /today commands to Commander
  2. Update ritual skills to use API (or keep reading exported markdown)

Cost Estimate

Item Cost Notes
Supabase tables $0 Free tier: 500MB, 50K rows. Current usage ~5%
Supabase realtime $0 Free tier: 200 concurrent connections
VPS compute $0 Already provisioned. Express server ~20MB RAM
LLM calls $0 No LLM needed for CRUD operations
Development ~2.5 hrs 6 phases at 15-30 min each (Ralph time)
Total: $0 incremental — All within existing infrastructure

Risks & Mitigations

Risk Impact Mitigation
Supabase free tier row limits Tasks accumulate in done column Archive done tasks older than 90 days to kanban_archive. Cron job.
Export cron drift Task Board.md briefly out of sync 60s cadence is fine. Worst case = 90s lag. Rituals can call /v1/export/write explicitly.
Dashboard reads during migration Stale data if API is down Keep VPS file API fallback. If API returns 5xx, fall back to parsing markdown.
Two sources of truth during migration Conflicting edits Phase 3 (import) is the cutoff. After import, disable direct markdown editing.
Ralph dispatch creates duplicate tasks Same task in kanban + ralph_queue ralph_queue_id FK prevents double-dispatch. UI disables button if already dispatched.
Position gaps after deletes/moves Messy ordering Reorder endpoint normalizes positions (0, 1, 2...) on write. No gaps accumulate.

Open Questions

Decisions Pending
  1. Agenda items as tasks? Design says YES — they get their own column with agenda_person/agenda_topic fields. Simpler than a separate table.
  2. Supabase anon key for dashboard reads? Direct reads via anon key for speed, writes via VPS proxy for auth.
  3. Task Board.md header/footer? The ## Reference section should be hardcoded in the exporter (static content, not tasks).
  4. Effort estimates for BOT integration? Add effort_minutes INT to kanban_tasks now. Cheap to add, expensive to migrate later.
  5. Scratchpad capture? Keep separate for Phase 1. Scratchpad is ephemeral capture, inbox is triaged-but-unassigned — different semantics.