Lightweight self-contained online clipboard with a REST API, real-time sync, HTMX UI, and PWA support.
# With Docker
docker compose up -d --build
# Local (requires Bun)
bun install
bun run dev- Accounts — register/login at
/register,/login, dashboard at/account; optional Google/GitHub OAuth - Account security — login lockout after repeated failures, sessions that expire after 30 days and can be revoked ("sign out everywhere"), self-service account deletion, plus email confirmation and password reset when
RESEND_API_KEYis set - Account settings — edit the display name, change the password in-app, and move the account to a new email address (confirmed from the new address) at
/account/settings
With a mailer configured, a new email/password sign-up cannot sign in until the address is confirmed. The link is single-use and expires in 24 hours; accounts left unconfirmed for 7 days are deleted, so a squatted address frees itself up. Google/GitHub accounts arrive pre-confirmed because the provider only returns verified addresses, and completing a password reset also confirms the address. Without RESEND_API_KEY and MAIL_FROM the whole flow is skipped and sign-ups are usable immediately.
- API keys — create via account UI or
POST /api/v1/auth/api-keys(Bearer session) - Teams — create workspace, invite members by email, vanity URLs
/{team}/{clip-name} - E2E encryption — AES-256-GCM client-side; passphrase wraps the key (never sent to the server)
- Version history — auto-saved every 5s while editing; restore from sidebar
- Register and create a team at
/account - Open
/teams/{team-slug}and create a clip - Share
https://webklip.com/{team}/{clip-name}
Owners and admins invite by email from /teams/{team-slug}. The invite link is bound to that address, works once, and expires in 7 days; it is emailed when a mailer is configured and always shown in the UI so it can be shared by hand. Roles are admin (manage members), member (read and write clips), and viewer (read only) — ownership stays with the creator and cannot be granted through an invite.
Team pages and member lists are visible to members only; everyone else gets a 404. Admins can revoke pending invites, change a member's role, and remove members; members can leave. The owner can additionally rename the team, transfer it to another member (becoming an admin in the process), or delete it outright, which also removes every clip under /{team}/.
A team's slug is fixed once created, because it forms the first half of every team clip URL.
- In clip settings → Protect, choose Passphrase
- Keep the auto-generated memorable phrase, or set your own (short secrets require acknowledging offline-crack risk)
- Share the clean clip URL and the passphrase separately — recipients unlock in the browser; Webklip never receives the passphrase
Public Klipwall clips cannot use passphrase E2E. File uploads are disabled while a clip is encrypted.
Legacy server PIN clips (pre-passphrase) still unlock via the PIN form / X-Clip-Pin. New protection from the UI is always true E2E.
- View limits — 1, 3, 10 reads or unlimited (API reads only)
- Webhooks —
POSTJSON to your URL onread,burned,expiredevents - File/image upload — drop, browse, or paste a screenshot (Ctrl+V / Cmd+V); preview images, PDFs, text, and media in a modal (not available on E2E-encrypted clips)
{
"event": "read",
"slug": "myclip",
"timestamp": "2026-07-07T12:00:00.000Z",
"viewCount": 2,
"burnOnRead": false,
"maxViews": 3,
"burned": false
}GET /api/health— health checkGET /api/v1/clips/:slug— read clip (burn-on-read if enabled)POST /api/v1/clips/:slug— create clipPUT /api/v1/clips/:slug— update clipDELETE /api/v1/clips/:slug— delete clipGET /api/v1/files/:slug/:id— download attached file
| Variable | Default | Description |
|---|---|---|
DATA_DIR |
./data |
SQLite DB + file uploads |
PORT |
3000 |
HTTP port |
NODE_ENV |
— | Set to production in production |
SESSION_SECRET |
— | Required in production. HMAC signing for cookies |
SECURE_COOKIES |
false |
Set true behind HTTPS reverse proxy |
CONTACT_EMAIL |
contact@logimaxx.ro |
Legal/security contact and destination for the /contact form |
SITE_URL |
https://webklip.com |
Public site URL (no trailing slash) — writes sitemap.xml at build time; OAuth redirect base |
ENABLE_AUTH_API |
false |
Enable POST /api/v1/auth/register and API key API |
RESEND_API_KEY / MAIL_FROM |
— | Resend API key and sender address — both required for password reset and the contact form |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
— | Enable “Continue with Google” (redirect: {SITE_URL}/auth/google/callback) — see docs/OAUTH.md |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
— | Enable “Continue with GitHub” (redirect: {SITE_URL}/auth/github/callback) — see docs/OAUTH.md |
UMAMI_WEBSITE_ID |
— | Umami website ID — enables analytics when set |
UMAMI_SCRIPT_URL |
— | Full URL to script.js (or set UMAMI_URL instead) |
CORS_ORIGIN |
— | Allow cross-origin API access from this origin |
MAX_FILE_SIZE_MB |
10 |
Max size per uploaded file |
MAX_TOTAL_FILES_MB |
50 |
Max total size of all attachments on a clip |
RATE_LIMIT_CLIPS_PER_HOUR |
30 |
Clip creation limit per IP |
RATE_LIMIT_API_PER_HOUR |
200 |
API limit per IP |
See SECURITY.md for the full deployment checklist, threat model, and responsible disclosure process.
To move an existing Docker deployment (volumes + .env) to another host, see docs/MIGRATION.md.
Marketing pages live under website/; see docs/WEBSITE_SPLIT.md for the app/website layout and planned edge split.
Quick checklist:
- Generate a strong
SESSION_SECRET(openssl rand -base64 32) - Set
NODE_ENV=productionand terminate TLS at a reverse proxy - Set
SECURE_COOKIES=trueor forwardX-Forwarded-Proto: https - Use a persistent volume for
DATA_DIR - Set
CONTACT_EMAILfor legal pages
docker compose -f docker-compose.yml -f docker-compose.scale.yml up -dbun run build:assets # production: minify + random hash folder
bun run build:assets --dev # dev: dist/assets/dev/