609 lines
13 KiB
Markdown
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.
|