clickclack/SPEC.md
2026-05-08 05:36:16 +01:00

609 lines
13 KiB
Markdown

# ClickClack Spec
ClickClack is a self-hostable, API-first chat app for internal testing, small teams, and communities. It mixes Slack-style productivity with Discord-style warmth, plus a light crustacean theme.
## Goals
- Run as a tiny single binary with first-class SQLite storage.
- Offer a hosted/server deployment path with Postgres later.
- Provide reliable realtime text chat with Slack-style threads.
- Keep the backend API-first and frontend-framework-independent.
- Ship a TypeScript SDK for bots, integrations, and community tooling.
- Feel playful and memorable without sacrificing dense, practical chat workflows.
## Locked V1 Decisions
- First implementation target: realtime channel chat plus Slack-style threads, not skeleton-only.
- Auth starts CLI-manageable: local owner/user bootstrap and invite/token management from `clickclack admin ...`.
- GitHub OAuth is optional V1, after local auth is usable.
- Frontend is Svelte 5 + Vite SPA. No SvelteKit server layer.
- API contract is OpenAPI-first, with `packages/protocol/openapi.yaml` as the source of truth.
- IDs use ULID-style sortable text IDs with semantic prefixes such as `usr_`, `wsp_`, `chn_`, `msg_`, `evt_`.
- Message body format starts as Markdown. Clients render a safe Markdown subset.
- Search, uploads, and DMs are V1 product scope, but come after the realtime channel/thread vertical slice is working.
- Monorepo layout is the canonical repo shape.
## Non-Goals For V1
- Voice/video rooms.
- Full Slack, Discord, or Mattermost server compatibility.
- Federation.
- End-to-end encryption.
- Enterprise compliance features.
- Multi-node websocket fanout.
## Product Shape
### Naming
- Product: ClickClack.
- Primary domain: `clickclack.chat`.
- Backend/protocol codename, if needed: Clawwire.
- Theme: lobster/crustacean accents, not renamed core UX primitives.
### First Users
- Internal testing groups.
- Self-hosted teams.
- Small communities.
- Bot-heavy hacker spaces.
### UX Model
- Multi-workspace.
- Workspace contains channels.
- Channel timeline shows root messages only.
- Every root message can have one Slack-style thread.
- Thread opens in a right-side pane.
- Thread replies are one-level only; no nested reply trees.
- Presence and typing are ephemeral.
- Light/dark themes from day one.
Use familiar terms for core navigation:
- Workspace
- Channel
- Thread
- Message
- Reaction
- Bot
Use crustacean flavor in:
- Logo/mascot.
- Empty states.
- Loading states.
- Reaction pack.
- Sounds.
- Onboarding copy.
- Optional statuses like `molting`, `lurking`, `afk`.
## V1 Vertical Slice
The first useful build should support:
- Create/select workspace.
- Create/select channel.
- Send Markdown text message.
- Realtime message delivery over WebSocket.
- Open message thread in right pane.
- Send thread reply.
- Persist everything in SQLite.
- Reload/reconnect and recover state.
- CLI-manageable local auth/bootstrap.
- Embedded web app served by Go.
After that vertical slice is stable, V1 expands to:
- Direct messages.
- SQLite FTS5 message search.
- Local file uploads and message attachments.
- GitHub OAuth as an optional login path.
## Architecture
```text
clickclack/
apps/
api/ # Go backend and single-binary entrypoint
web/ # Svelte SPA
packages/
protocol/ # OpenAPI spec and event schemas
sdk-ts/ # TypeScript SDK, generated client + friendly wrapper
docs/
architecture/
api/
infra/
migrations/
sqlite/
postgres/ # later
```
## Backend
Language: Go.
Initial runtime:
- Single Go process.
- `modernc.org/sqlite`.
- Embedded migrations.
- Embedded Svelte build via `go:embed`.
- Local upload storage.
- In-process websocket hub.
Future hosted runtime:
- Postgres.
- Object storage.
- External queue/pubsub only when needed.
- Multi-node websocket fanout later.
### Suggested Go Libraries
- HTTP router: `chi`.
- SQLite: `modernc.org/sqlite`.
- Postgres later: `pgx`.
- Queries: start handwritten or `sqlc` once schema settles.
- Migrations: embedded SQL migrations with a tiny internal runner, or `goose` if the runner grows.
- IDs: ULID-style sortable text IDs with type prefixes.
### CLI
```text
clickclack serve
--addr :8080
--data ./data
--db sqlite://./data/clickclack.db
clickclack migrate
--db sqlite://./data/clickclack.db
clickclack admin bootstrap
--name "Peter"
--email steipete@gmail.com
clickclack admin user create
--name "Ari"
--email ari@example.com
clickclack admin invite create
--workspace wsp_...
```
Default `clickclack serve` should be enough for local development. Production-like local use should bootstrap an owner through the CLI before exposing the instance.
## Frontend
Framework: Svelte 5 SPA.
Use plain Svelte + Vite unless SvelteKit offers clear value without adding server-side complexity. The Go server owns HTTP/API/auth and serves static assets.
Frontend responsibilities:
- Render workspace/channel/thread UI.
- Keep local client cache/projection.
- Use HTTP API for writes and fetches.
- Use WebSocket for realtime events.
- Recover by refetching from API after reconnect.
Frontend should not own durable chat truth.
## API
Contract: OpenAPI first.
Source of truth:
```text
packages/protocol/openapi.yaml
```
Generate:
- Go request/response types or validators where useful.
- TypeScript API client.
- SDK docs.
Initial REST shape:
```text
GET /api/me
GET /api/workspaces
POST /api/workspaces
GET /api/workspaces/{workspace_id}
GET /api/workspaces/{workspace_id}/channels
POST /api/workspaces/{workspace_id}/channels
PATCH /api/channels/{channel_id}
GET /api/channels/{channel_id}/messages?before=&after_seq=&limit=
POST /api/channels/{channel_id}/messages
PATCH /api/messages/{message_id}
DELETE /api/messages/{message_id}
GET /api/messages/{message_id}/thread
POST /api/messages/{message_id}/thread/replies
POST /api/messages/{message_id}/reactions
DELETE /api/messages/{message_id}/reactions/{emoji}
GET /api/realtime/events?after_cursor=
POST /api/realtime/ephemeral
GET /api/realtime/ws
GET /api/search?workspace_id=&q=&limit=
POST /api/uploads
GET /api/uploads/{upload_id}
GET /api/dms
POST /api/dms
GET /api/dms/{conversation_id}/messages?before=&after_seq=&limit=
POST /api/dms/{conversation_id}/messages
```
## Realtime
Realtime must be recoverable.
Rules:
- WebSocket is a notification/update pipe.
- SQLite/Postgres is source of truth.
- Every durable event is recoverable through HTTP.
- Client reconnects with last seen cursor.
- If cursor is too old or unknown, server returns `resync_required`.
Send flow:
1. Client calls `POST /api/channels/{id}/messages`.
2. Server validates auth and membership.
3. Server transaction:
- insert message
- assign per-channel sequence
- insert event into outbox/events table
- update thread/channel summary state
4. In-process dispatcher broadcasts event to websocket subscribers.
5. Client reconciles optimistic message with server event.
Event shape:
```json
{
"id": "evt_...",
"cursor": "...",
"type": "message.created",
"workspace_id": "w_...",
"channel_id": "c_...",
"seq": 124,
"created_at": "2026-05-08T12:00:00Z",
"payload": {
"message_id": "m_..."
}
}
```
Initial durable events:
- `message.created`
- `message.updated`
- `message.deleted`
- `thread.reply_created`
- `thread.state_updated`
- `reaction.added`
- `reaction.removed`
- `channel.created`
- `channel.updated`
Ephemeral events:
- `typing.started`
- `typing.stopped`
- `presence.changed`
Ephemeral events are not persisted and may be dropped.
## Data Model
Initial tables:
```text
users
id
display_name
avatar_url
created_at
identities
id
user_id
provider
provider_subject
email
created_at
workspaces
id
name
slug
created_at
workspace_members
workspace_id
user_id
role
created_at
channels
id
workspace_id
name
kind
created_at
archived_at
messages
id
workspace_id
channel_id
author_id
parent_message_id
thread_root_id
channel_seq
thread_seq
body
body_format
created_at
edited_at
deleted_at
thread_state
root_message_id
reply_count
last_reply_at
last_reply_author_ids_json
reactions
message_id
user_id
emoji
created_at
events
id
cursor
workspace_id
channel_id
type
payload_json
created_at
uploads
id
workspace_id
owner_id
filename
content_type
byte_size
storage_path
created_at
message_attachments
message_id
upload_id
created_at
direct_conversations
id
workspace_id
created_at
direct_conversation_members
conversation_id
user_id
created_at
```
Thread rules:
- Root message has `parent_message_id = null`.
- Root message has `thread_root_id = id`.
- Thread reply has `parent_message_id = root_message_id`.
- Thread reply has `thread_root_id = root_message_id`.
- No nested replies in V1.
## Storage
SQLite is first-class.
SQLite requirements:
- Use `modernc.org/sqlite`.
- Enable WAL mode.
- Use a single writer discipline.
- Keep transactions short.
- Prefer portable SQL.
- Avoid Postgres-only behavior in core paths.
- Add separate Postgres migrations later rather than forcing one dialect.
Local file layout:
```text
data/
clickclack.db
uploads/
logs/
```
## Auth
V0:
- CLI owner bootstrap.
- CLI user/invite management.
- Dev/local auth for quick testing, gated to local/dev mode.
- CLI-generated magic-link tokens.
- Bearer session tokens and HTTP-only cookie sessions.
V1:
- Magic-link token issuance and consume flow, with CLI/local delivery first.
- GitHub OAuth as optional login, enabled via self-host config.
- SMTP or provider-backed email delivery later, once deployment mail settings are known.
- Optional local email/password only if needed for fully offline/self-hosted deployments.
Auth principles:
- Workspace membership checked on every API write.
- WebSocket subscribe validates workspace/channel access.
- Recheck permissions for channel/thread fetches.
## SDK
First SDK: TypeScript.
Location:
```text
packages/sdk-ts
```
Layering:
- Generated OpenAPI types.
- Friendly wrapper.
- WebSocket/event subscription helper.
Example API:
```ts
const client = new ClickClackClient({ baseUrl, token });
await client.channels.sendMessage(channelId, {
body: "click clack",
});
client.events.subscribe({
workspaceId,
onEvent(event) {
// handle event
},
});
```
SDK must not depend on Svelte.
## Mattermost Compatibility
Do not clone the full Mattermost API in V1.
Do support:
- Incoming webhook compatibility.
- Simple slash-command callback shape.
- Import helpers for exports if useful.
Do not support early:
- Existing Mattermost clients connecting directly.
- Full REST API compatibility.
- Full permission/model compatibility.
## Design Direction
ClickClack should feel:
- Fast.
- Dense.
- Friendly.
- Slightly weird.
- More polished tool than joke app.
Visual direction:
- Light and dark themes.
- Neutral UI base.
- Coral, shell, brine, ink accents.
- Crustacean mascot and iconography used sparingly.
- Avoid novelty typography.
- Avoid making normal controls hard to understand.
UI layout:
```text
left sidebar: workspaces / channels
center: channel timeline
right pane: thread
bottom: composer
top: channel title, members, search
```
## Development Milestones
### M0: Skeleton
- Monorepo.
- Go server boots.
- Svelte app builds.
- Go embeds and serves web assets.
- SQLite opens and migrates.
### M1: Durable Chat
- Workspaces/channels/messages schema.
- REST create/list messages.
- Basic dev auth.
- Message timeline UI.
### M2: Realtime
- WebSocket endpoint.
- Event outbox.
- Live message updates.
- Reconnect and cursor recovery.
### M3: Threads
- Root messages and one-level replies.
- Thread pane.
- Thread reply counts and last reply state.
### M4: Search, Uploads, DMs
- SQLite FTS5 message search.
- Local upload storage.
- Message attachments.
- Direct message conversations.
### M5: Self-Host Polish
- First-run owner setup.
- CLI-generated magic-link auth.
- Config file/env.
- Docker image.
- Backups/export.
### M6: SDK And Integrations
- OpenAPI generation.
- TypeScript SDK.
- Incoming webhooks.
- Basic bot example.
## Answered Questions
- Setup starts with CLI owner bootstrap. A setup UI can be added later.
- Markdown is the initial rich text format.
- DMs are V1 scope, after channel chat and threads.
- Search starts with SQLite FTS5.
- Uploads are V1 scope, after core chat is solid.
- OpenAPI remains source of truth from the first scaffold.
- TypeScript compilation uses `tsgo`; lint/format use `oxlint` and `oxfmt`.
- GitHub OAuth ships in V1 as an optional configured auth provider.
## Open Questions
- Whether to add generated Go request/response validation from OpenAPI in V1 or keep the first backend on hand-written handlers.