| read_when |
|
|---|
ClickClack ships as one Go binary that embeds the Svelte SPA and the SQL migrations. The deployment story is "drop a binary on a box, point it at a data directory, run it behind a reverse proxy."
Public surfaces:
clickclack.chat— product website.app.clickclack.chat— chat app. The same app is also available at/appfor local development and simple single-host deployments.docs.clickclack.chat— documentation site built bypnpm docs:site.
pnpm install
pnpm build # builds the SPA into apps/api/internal/webassets/dist
go build -o clickclack ./apps/api/cmd/clickclack
./clickclack serve --addr :8080 --data /var/lib/clickclackThe Go build step requires the SPA dist/ to be present because webassets
uses go:embed. The pnpm build script copies apps/web/dist into
apps/api/internal/webassets/dist; CI must run it before go build.
The embedded frontend is a SvelteKit static SPA. Reverse proxies should pass
unknown paths through to the ClickClack binary, because direct visits to app
routes such as /app/T.../C..., /app/T.../D..., and /app/T.../M... are
resolved by the frontend fallback. Older internal-ID links such as
/app/wsp_.../chn_..., /app/wsp_.../dm_..., and /app/wsp_.../msg_...
are still accepted and canonicalized by the app after API permission checks.
pnpm build defaults the SvelteKit app version to dev so repeated local
builds do not rewrite embedded asset filenames when source code has not
changed. Release automation should set CLICKCLACK_WEB_VERSION to the commit
or tag being shipped so long-lived open browser tabs can detect a newly
deployed frontend bundle:
CLICKCLACK_WEB_VERSION="$(git rev-parse --short=12 HEAD)" pnpm buildGoReleaser is configured in .goreleaser.yml. It builds clickclack for
Linux, macOS, Windows, and FreeBSD on amd64 and arm64, with Windows
archives emitted as .zip and the others as .tar.gz. Linux .deb and
.rpm packages are generated through nfpm.
pnpm install
CLICKCLACK_WEB_VERSION="$(git rev-parse --short=12 HEAD)" \
goreleaser release --snapshot --cleanThe GoReleaser config runs pnpm build before compiling so the embedded SPA
is refreshed. The GitHub release workflow sets CLICKCLACK_WEB_VERSION from
the checked-out commit before invoking GoReleaser. Publishing is handled by
.github/workflows/release.yml on v* tags or manual dispatch with an
existing tag.
The provided Dockerfile is multi-stage:
docker build \
--build-arg CLICKCLACK_WEB_VERSION="$(git rev-parse --short=12 HEAD)" \
-t clickclack .
docker run --rm -p 8080:8080 -v clickclack-data:/app/data clickclackStages:
node:24-alpine— installs pnpm dependencies and runspnpm build.golang:1.26.5-alpine— builds the Go binary, importing the SPA dist.alpine:3.23— runtime image, runs as theclickclackuser, exposes8080, mounts/app/dataas a volume.
Override the entrypoint command to run admin tasks:
docker run --rm -v clickclack-data:/app/data clickclack \
admin bootstrap --name "Peter" --email steipete@gmail.comFor the isolated non-production small-VM topology, deterministic synthetic seed, OpenClaw/ClawRouter SecretRef contract, canary, and teardown, see FakeCo staging. It uses this same Docker image and SQLite adapter; it does not add a second ClickClack cloud runtime. The guarded FakeCo-only AWS owner is documented in deploy/fakeco/aws/README.md.
GET /healthz reports process liveness. GET /readyz checks database
connectivity and returns 503 without the database error when unavailable.
Both responses include an X-Correlation-ID; callers may supply a safe ID or
let the server generate one.
Set CLICKCLACK_METRICS_ENABLED=true only on a private operator network to
expose Prometheus text at /metrics. Metrics use normalized route patterns and
status classes; they do not label users, workspaces, channels, messages, query
values, or body content. When disabled, /metrics returns 404.
SQLite layout:
<data>/
clickclack.db # SQLite database (WAL files alongside)
uploads/ # local files for /api/uploads
logs/ # reserved; nothing writes here today
Back this directory up. SQLite WAL means a snapshot of the directory is consistent enough, but prefer the online backup:
clickclack backup --data /var/lib/clickclack --out /var/backups/clickclack-$(date +%F).dbPostgres layout:
CLICKCLACK_DB='postgres://user:pass@db.example.com:5432/clickclack?sslmode=require' \
clickclack serve --addr :8080 --data /var/lib/clickclackThe Postgres adapter stores users, messages, events, auth, search, and chat
metadata in Postgres. Use provider snapshots or pg_dump for Postgres
backups; clickclack backup is SQLite-only.
R2 upload layout:
CLICKCLACK_DB='postgres://user:pass@db.example.com:5432/clickclack?sslmode=require' \
CLICKCLACK_UPLOADS='r2://clickclack-uploads/prod' \
CLICKCLACK_R2_ACCOUNT_ID='91b59577e757131d68d55a471fe32aca' \
CLICKCLACK_R2_ACCESS_KEY_ID='...' \
CLICKCLACK_R2_SECRET_ACCESS_KEY='...' \
CLICKCLACK_DEV_BOOTSTRAP=false \
clickclack serve --addr :8080 --data /var/lib/clickclackR2 stores upload bytes; Postgres stores upload metadata and message attachment
links. Requests still go through /api/uploads/{id} so workspace/member
authorization stays server-side.
The official hosted ClickClack deployment is operated separately from the self-hosting examples in this document. Hosted edge configuration is outside this repository change; the OAuth and OpenAPI changes described here do not modify or prove it. Hosted edge controls, rollout, and verification must be handled in the infrastructure that owns hosted traffic.
An OpenAPI 429 response documents a response that a deployment edge may
return when configured. It is not evidence that the hosted deployment currently
enforces an edge rate limit.
An internet-facing self-hosted deployment needs a trusted reverse proxy or
equivalent edge for TLS and request-size enforcement. The WebSocket endpoint
enforces the request host by default and also allows CLICKCLACK_PUBLIC_URL as
an origin when configured.
Terminate TLS only at infrastructure you trust, redirect HTTP to HTTPS, and
send HSTS from the public HTTPS origin after confirming every subdomain covered
by the policy is HTTPS-ready. ClickClack intentionally does not trust arbitrary
X-Forwarded-For values for security decisions. The setup-code claim limiter
uses the transport peer by default. It accepts a forwarded client IP only when
the peer is loopback and X-Real-IP matches a single-IP X-Forwarded-For,
which is the contract used by the bundled Nginx example. Other proxy topologies
must enforce their own edge limit or expose a separately verified client
identity; arbitrary forwarding headers remain ignored.
deploy/nginx/clickclack.conf.example
is an optional starting point for a directly operated, self-hosted Nginx
deployment. It is not ClickClack's hosted production configuration and does not
describe or configure the hosted edge.
The example includes TLS, WebSocket proxying, a 64 MiB request limit,
query-free access logs, explicit forwarding headers, and OAuth plus setup-code
claim rate limits.
Replace its hostname and certificate paths, confirm the upstream address, run
nginx -t, and reload only after the syntax check succeeds. Its OAuth start
bucket allows one request per second with a burst of eight, while desktop
redemption allows five requests per second with a burst of twenty. These are
self-hosting starting values, not universal capacity targets.
The limits use the directly connected client IP. If another proxy sits in front
of Nginx, configure ngx_http_realip_module with only that proxy's published
address ranges before relying on the limits. Never accept an arbitrary
client-supplied forwarding header as the rate-limit key.
If you want GitHub login, set:
CLICKCLACK_PUBLIC_URL=https://chat.example.com
# Optional only when trusted ClickClack instances share this hostname:
# CLICKCLACK_COOKIE_NAMESPACE=production
CLICKCLACK_GITHUB_CLIENT_ID=...
CLICKCLACK_GITHUB_CLIENT_SECRET=...
# Optional org gate:
# CLICKCLACK_GITHUB_ALLOWED_ORG=openclaw
# Optional moderator org for open guest login:
# CLICKCLACK_GITHUB_MODERATOR_ORG=openclawConfigure the GitHub OAuth app callback to
<public-url>/api/auth/github/callback. Without CLICKCLACK_GITHUB_ALLOWED_ORG,
any GitHub account can sign in and is joined to an isolated Guests workspace
with a guest channel. When CLICKCLACK_GITHUB_MODERATOR_ORG is set, members
of that org become guest-workspace moderators and non-members start as
post-limited guests until approved. When the org gate is set, ClickClack asks
GitHub for read:org and only accepts active members of that org. See
features/auth.md and
features/moderation.md.
CLICKCLACK_PUBLIC_URL is startup-validated as an exact origin. Non-loopback
origins must use HTTPS and cannot contain a path, credentials, query, or
fragment. GitHub OAuth credentials are rejected without it.
For separate public frontend and API origins, keep CLICKCLACK_PUBLIC_URL as
the browser application origin and add the API address:
CLICKCLACK_PUBLIC_URL=https://chat.example.com
CLICKCLACK_PUBLIC_API_URL=https://api.example.comThe frontend ingress proxies SPA and asset requests to the ClickClack server,
which injects the configured API base into runtime HTML. The API ingress sends
/api/* and /api/realtime/ws to the same server. When the API URL has a base
path, the API ingress strips that prefix before forwarding. GitHub OAuth's
callback must be registered at
<CLICKCLACK_PUBLIC_API_URL>/api/auth/github/callback; successful browser
login returns to CLICKCLACK_PUBLIC_URL.
Browser API access is credentialed and exact-origin only:
- CORS allows only the configured
CLICKCLACK_PUBLIC_URL; no wildcard or reflected arbitrary origin is used. - Preflights allow only the API's normal methods and the
Authorization,Content-Type,X-ClickClack-CSRF, andX-Request-IDheaders. - Session-cookie mutations still require
X-ClickClack-CSRF: 1and the exact configured frontendOrigin. - Realtime WebSocket upgrades accept that same exact frontend origin.
- Same-host port splits retain
SameSite=Lax. Different HTTPS hostnames useSameSite=None; Secure; browser third-party-cookie policy can still block unrelated-site topologies, so related frontend/API hostnames are preferred.
The API also serves the embedded SPA at its own origin, but a split deployment
must treat CLICKCLACK_PUBLIC_URL as the only supported browser origin.
GitHub OAuth apps have one configured callback URL. Each ClickClack public
origin, including a distinct non-default port, therefore needs matching OAuth
app configuration. Never point multiple unrelated public origins at one
instance and rely on the request Host header to choose a callback.
HTTP cookies do not include ports in their scope. When multiple trusted
ClickClack instances share one hostname, assign each a unique, stable
CLICKCLACK_COOKIE_NAMESPACE; all replicas of one instance must use the same
value. This avoids accidental cookie-name collisions but does not isolate
mutually untrusted services. Use separate hostnames, or separate registrable
domains for stronger isolation.
OAuth state and desktop grants are stored in the configured database, so callbacks can land on a different replica and survive process restarts. If a deployment configures edge rate limiting, cover:
GET /api/auth/github/startGET /api/auth/github/desktop/startPOST /api/auth/github/desktop/consume
Use a client identity that is trustworthy for the complete deployment path and leave enough headroom for legitimate users behind shared networks. The Go server does not implement IP-based OAuth limiting because it cannot assume that the directly connected address or a forwarded header identifies the original client in every deployment.
The database rejects new work at 8,192 pending OAuth transactions, eight pending starts per browser binding, and 4,096 pending desktop grants. These are capacity safeguards, not recommended edge rate limits. Monitor pending capacity and do not raise the limits without load testing.
When edge limiting is configured, alert on sustained 429 responses. Always
alert on sustained 503 or
clickclack_github_oauth_events_total{event="capacity_rejected"} activity, and
on pending capacity before it reaches the hard limit.
Configure proxy and edge logs to omit query strings on every GitHub OAuth
route, including /api/auth/github/callback. Query strings can contain
short-lived authorization codes or desktop verifier challenges. Never log
Authorization, Cookie, or Set-Cookie headers. ClickClack's request logger
records route patterns without query strings.
clickclack serve applies migrations on boot. For zero-downtime deploys, run
clickclack migrate ahead of the new binary so the old binary doesn't see
unexpected tables. SQLite migrations live in
apps/api/internal/store/sqlite/migrations/; Postgres migrations live in
apps/api/internal/store/postgres/migrations/. Both are append-only.
The durable realtime event log is for reconnect recovery, not permanent
message history. Message history stays in messages; old events can be
removed after the offline-recovery window you operate against:
clickclack admin events prune --workspace wsp_... --older-than-days 30 --keep-latest 10000Run this from maintenance automation after backups. Clients with cursors older than the retained event window should resync through the message APIs.
# hot backup
clickclack backup --out /var/backups/clickclack-$(date +%F).db
# JSON dump (good for sanity, not for restore)
clickclack export --out /var/backups/clickclack-$(date +%F).jsonSQLite restore is a file swap: stop clickclack, replace
<data>/clickclack.db, delete any stale *.db-wal/*.db-shm, start it back
up. Postgres restore uses your database provider's restore flow or psql
from a pg_dump output.