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.
- 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.
- 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.yamlas 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.
- Voice/video rooms.
- Full Slack, Discord, or Mattermost server compatibility.
- Federation.
- End-to-end encryption.
- Enterprise compliance features.
- Multi-node websocket fanout.
- Product: ClickClack.
- Primary domain:
clickclack.chat. - Backend/protocol codename, if needed: Clawwire.
- Theme: lobster/crustacean accents, not renamed core UX primitives.
- Internal testing groups.
- Self-hosted teams.
- Small communities.
- Bot-heavy hacker spaces.
- 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.
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.
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
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.
- HTTP router:
chi. - SQLite:
modernc.org/sqlite. - Postgres later:
pgx. - Queries: start handwritten or
sqlconce schema settles. - Migrations: embedded SQL migrations with a tiny internal runner, or
gooseif the runner grows. - IDs: ULID-style sortable text IDs with type prefixes.
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.
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.
Contract: OpenAPI first.
Source of truth:
packages/protocol/openapi.yaml
Generate:
- Go request/response types or validators where useful.
- TypeScript API client.
- SDK docs.
Initial REST shape:
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 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:
- Client calls
POST /api/channels/{id}/messages. - Server validates auth and membership.
- Server transaction:
- insert message
- assign per-channel sequence
- insert event into outbox/events table
- update thread/channel summary state
- In-process dispatcher broadcasts event to websocket subscribers.
- Client reconciles optimistic message with server event.
Event shape:
{
"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.createdmessage.updatedmessage.deletedthread.reply_createdthread.state_updatedreaction.addedreaction.removedchannel.createdchannel.updated
Ephemeral events:
typing.startedtyping.stoppedpresence.changed
Ephemeral events are not persisted and may be dropped.
Initial tables:
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.
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:
data/
clickclack.db
uploads/
logs/
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.
First SDK: TypeScript.
Location:
packages/sdk-ts
Layering:
- Generated OpenAPI types.
- Friendly wrapper.
- WebSocket/event subscription helper.
Example API:
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.
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.
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:
left sidebar: workspaces / channels
center: channel timeline
right pane: thread
bottom: composer
top: channel title, members, search
- Monorepo.
- Go server boots.
- Svelte app builds.
- Go embeds and serves web assets.
- SQLite opens and migrates.
- Workspaces/channels/messages schema.
- REST create/list messages.
- Basic dev auth.
- Message timeline UI.
- WebSocket endpoint.
- Event outbox.
- Live message updates.
- Reconnect and cursor recovery.
- Root messages and one-level replies.
- Thread pane.
- Thread reply counts and last reply state.
- SQLite FTS5 message search.
- Local upload storage.
- Message attachments.
- Direct message conversations.
- First-run owner setup.
- CLI-generated magic-link auth.
- Config file/env.
- Docker image.
- Backups/export.
- OpenAPI generation.
- TypeScript SDK.
- Incoming webhooks.
- Basic bot example.
- 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 useoxlintandoxfmt. - GitHub OAuth ships in V1 as an optional configured auth provider.
- Whether to add generated Go request/response validation from OpenAPI in V1 or keep the first backend on hand-written handlers.