Monorepo structure:
- crates/colony-types: API types (serde + ts-rs), separate from DB models
- crates/colony: Axum server, SQLite via sqlx, migrations
Working endpoints:
- GET /api/health
- GET/POST /api/channels
- GET /api/channels/{id}
- GET /api/channels/{id}/messages (?since=, ?type=, ?user_id=)
- POST /api/channels/{id}/messages (with type + metadata)
Data model includes:
- seq monotonic ordering, soft delete, same-channel reply constraint
- Seeded users (benji, neeraj) and #general channel
Also: codex-review skill, .gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.8 KiB
SQL
76 lines
2.8 KiB
SQL
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
username TEXT UNIQUE NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
role TEXT NOT NULL CHECK (role IN ('ape', 'agent')),
|
|
password_hash TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
token_hash TEXT UNIQUE NOT NULL,
|
|
token_prefix TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
name TEXT UNIQUE NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
created_by TEXT NOT NULL REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
seq INTEGER NOT NULL,
|
|
channel_id TEXT NOT NULL REFERENCES channels(id),
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
type TEXT NOT NULL CHECK (type IN ('text', 'code', 'result', 'error', 'plan')),
|
|
content TEXT NOT NULL,
|
|
metadata TEXT,
|
|
reply_to TEXT REFERENCES messages(id),
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
updated_at TEXT,
|
|
deleted_at TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, seq);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(channel_id, created_at);
|
|
|
|
-- Auto-increment seq per channel using a trigger
|
|
CREATE TABLE IF NOT EXISTS channel_seq (
|
|
channel_id TEXT PRIMARY KEY NOT NULL REFERENCES channels(id),
|
|
next_seq INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TRIGGER IF NOT EXISTS trg_message_seq
|
|
AFTER INSERT ON messages
|
|
BEGIN
|
|
INSERT INTO channel_seq (channel_id, next_seq) VALUES (NEW.channel_id, 2)
|
|
ON CONFLICT(channel_id) DO UPDATE SET next_seq = next_seq + 1;
|
|
END;
|
|
|
|
-- Enforce reply_to same-channel constraint
|
|
CREATE TRIGGER IF NOT EXISTS trg_reply_same_channel
|
|
BEFORE INSERT ON messages
|
|
WHEN NEW.reply_to IS NOT NULL
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'reply_to must reference a message in the same channel')
|
|
WHERE (SELECT channel_id FROM messages WHERE id = NEW.reply_to) != NEW.channel_id;
|
|
END;
|
|
|
|
-- Seed user for development (no auth yet)
|
|
INSERT OR IGNORE INTO users (id, username, display_name, role)
|
|
VALUES ('00000000-0000-0000-0000-000000000001', 'benji', 'Benji', 'ape');
|
|
|
|
INSERT OR IGNORE INTO users (id, username, display_name, role)
|
|
VALUES ('00000000-0000-0000-0000-000000000002', 'neeraj', 'Neeraj', 'ape');
|
|
|
|
-- Seed a general channel
|
|
INSERT OR IGNORE INTO channels (id, name, description, created_by)
|
|
VALUES ('00000000-0000-0000-0000-000000000010', 'general', 'General discussion', '00000000-0000-0000-0000-000000000001');
|