Server mode¶
clipboarder can run as a shared HTTP backend. Multiple clients connect with bearer tokens; each token is scoped to its own namespace, and namespaces are fully isolated — even the FTS index honors them.
Run it on your laptop for cross-device sync, or on a small server for a team shared clipboard.
Quick start¶
# 1. Create your first token (binds it to a namespace; namespace is created on first use)
clipboarder admin token create --namespace alice --label "Alice's MacBook"
# → prints tk_XXXX… — copy it now, you'll set it on the client below
# 2. Start the server
clipboarder serve --bind 127.0.0.1:7474
# 3. Point a client at it
export CLIPBOARDER_SERVER='http://127.0.0.1:7474'
export CLIPBOARDER_TOKEN='tk_XXXX…'
# Once the transparent-remote-store client lands, every `cb` command will
# automatically route through the server. Until then, use the REST API
# directly (see below).
Configuration¶
Default config path: ~/Library/Application Support/com.clipboarder.app/server.toml. Override with --config.
Generated by admin token create:
bind = "127.0.0.1:7474"
[[tokens]]
fingerprint = "tk_aB3xQ9XY"
hash = "$argon2id$v=19$m=19456,t=2,p=1$…" # argon2id PHC string
namespace = "alice"
label = "Alice's MacBook"
created_at = 1731436800000
last_used_at = 1731437452000
[[tokens]]
fingerprint = "tk_yZ1mN4LP"
hash = "$argon2id$v=19$m=19456,t=2,p=1$…"
namespace = "admin"
label = "Web UI"
admin = true
Tokens are argon2id-hashed at rest — the plaintext bearer is only shown once (when you run admin token create). The fingerprint is the first 11 chars of the bearer (tk_ + 8 random) and is the stable ID for admin token list / admin token revoke. The file is also chmod 600.
Live config reload¶
admin token revoke rewrites server.toml atomically. A running clipboarder serve polls the file's mtime every 2 s — within a couple of seconds the revoked token starts returning 401 from a live server, with no restart. Same goes for adding new tokens via admin token create.
REST endpoints¶
All routes are under /v1 and require Authorization: Bearer tk_…. The namespace is derived from the token — there's no namespace param in any URL.
| Method | Path | Body | Returns |
|---|---|---|---|
GET |
/v1/health |
— | ok (no auth required) |
GET |
/v1/whoami |
— | {namespace, label} |
GET |
/v1/items?q&kind&limit |
— | array of items |
POST |
/v1/items |
{content, kind?, meta?, source_app?} |
{id, inserted, kind} |
GET |
/v1/items/{id} |
— | item |
GET |
/v1/items/{id}/image |
— | raw PNG bytes (image-kind items) |
DELETE |
/v1/items/{id} |
— | 204 |
POST |
/v1/items/{id}/pin |
— | {id, pinned: true} |
DELETE |
/v1/items/{id}/pin |
— | {id, pinned: false} |
POST |
/v1/clear |
— | 204 (non-pinned only) |
GET |
/v1/stats |
— | {total, pinned, by_kind, namespace} |
GET |
/v1/watch |
— | Server-Sent Events stream of new items |
Query parameters¶
q— FTS5 query (prefix-matched). Omit for "most recent".kind—text/url/repo/code/color/image/file/pdf/music/video/email/pinned/all(defaultall).limit— max items returned. Capped at 10,000.
SSE — /v1/watch¶
Streams every new item captured under the caller's namespace, encoded as a JSON data: payload on an event: item line. Pings every 15 s as event: ping to keep proxies happy.
Admin¶
# Create a token
clipboarder admin token create --namespace alice --label "Alice's Mac"
# List tokens (full values redacted to prefix)
clipboarder admin token list
clipboarder admin token list --json
# Revoke by fingerprint (the 11-char `tk_…` shown in `list`) or full bearer
clipboarder admin token revoke tk_aB3xQ9XY
Revocation rewrites server.toml atomically and the running clipboarder serve picks it up within ~2 s via mtime polling — no restart needed.
Web admin console¶
The server bundles a single-page admin console at /admin for managing tokens and namespaces from a browser.
# 1. Mint an admin token (these gate /admin and /v1/admin/*).
clipboarder admin token create --admin --label "web ui"
# → tk_XXXXXXXXXXX… — copy it
# 2. Visit the server in a browser.
open http://127.0.0.1:7474/admin
# Paste the token to unlock.
What you can do from the UI:
- See every namespace with item count, pinned count, token count, and last activity
- See every token with fingerprint, namespace, label, last-used time, admin flag
- Create new tokens (the plaintext bearer is shown exactly once — copy it then)
- Revoke any token by clicking Revoke
The page is served from an embedded HTML asset — no separate web build, no JS framework, no static-file dependencies. The same clipboarder serve binary that runs the REST API serves the console.
JSON API for the same surface (each endpoint requires a token with admin = true):
| Method | Path | Body | Returns |
|---|---|---|---|
GET |
/v1/admin/tokens |
— | array of {fingerprint, namespace, label, created_at, last_used_at, admin} |
POST |
/v1/admin/tokens |
{namespace, label?, admin?} |
{bearer, fingerprint, namespace, label, created_at, admin} — bearer plaintext is only returned here |
DELETE |
/v1/admin/tokens/{fp} |
— | 204 / 404 |
GET |
/v1/admin/namespaces |
— | array of {namespace, items, pinned, last_activity, token_count} |
A non-admin bearer gets 403 on every admin endpoint (vs 401 for no/invalid token), so clients can distinguish "log in" from "not authorized."
Deployment¶
The server doesn't terminate TLS itself. Run it bound to 127.0.0.1 and front it with one of:
Caddy¶
That's it — Caddy handles Let's Encrypt automatically.
Nginx¶
server {
listen 443 ssl http2;
server_name clipboarder.example.com;
# SSL config…
location / {
proxy_pass http://127.0.0.1:7474;
proxy_http_version 1.1;
# For /v1/watch SSE
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 86400s;
}
}
systemd unit (Linux)¶
[Unit]
Description=clipboarder server
After=network.target
[Service]
ExecStart=/usr/local/bin/clipboarder serve --bind 127.0.0.1:7474
Restart=always
RestartSec=5
User=clipboarder
[Install]
WantedBy=multi-user.target
launchd (macOS)¶
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.clipboarder.server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/clipboarder</string>
<string>serve</string>
<string>--bind</string>
<string>127.0.0.1:7474</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
Save to ~/Library/LaunchAgents/com.clipboarder.server.plist and run launchctl load ….
Namespace isolation¶
Items, FTS index, pin state, stats — everything is scoped by namespace. The same content copied from two clients in different namespaces produces two distinct rows. The unique index on (namespace, content_hash) enforces this at the schema level.
Verified by scripts/test-server.sh — 27 assertions in CI:
- alice + bob both post → alice sees 2 items, bob sees 1
- alice searches "anthropic" → 1 hit
- bob searches "anthropic" → 0 hits (even though the bytes exist, they're in alice's namespace)
- alice DELETE
/v1/items/{bob_id}→ 404 - alice POST
/v1/clear→ bob's items untouched
Watch (real-time SSE)¶
cb watch consumes the server's /v1/watch Server-Sent Events stream and prints each new item as a JSON line. End-to-end latency is ~5 ms in practice — no polling, no missed events, no per-tick database scans.
# Tail every new item in your namespace as JSON Lines:
cb watch
# Filter to one kind:
cb watch --kind url
If you'd rather speak the wire protocol directly (e.g. from a language without a clipboarder client), the endpoint is:
Each new item is delivered as event: item with a JSON data: payload. Keep-alive event: ping is emitted every 15 s so reverse proxies don't drop the connection.
Roadmap¶
That's a wrap on the original v0.1 server roadmap. Future work (no specific plan yet): per-item TTLs, cross-namespace search for admin tokens, S3-backed image storage, signed-cookie auth for the web UI.