Textdrop docs
Reference for every feature. Short read. If you're looking at a specific form field, each one deep-links here.
Short links
A short link maps a five-character code (like LyaV0) to a destination URL. Visitors who open https://textdrop.co/LyaV0 are 302-redirected to the destination. Every visit is recorded for analytics.
Alias rules
The Optional alias field lets you pick your own code instead of the random one. Rules:
- 3–50 characters
- letters, digits, underscore (
_), hyphen (-) - must be unique across the whole service (409 Conflict on collision)
- reserved prefixes like
/auth,/admin,/dashboard,/api,/static,/pcan't be used
Leave empty to get a random 5-char base62 code.
Visit password
Gates the redirect behind a password. When a protected link is visited:
- A password form renders instead of the 302.
- On correct entry, the server sets a signed unlock cookie scoped to that specific code (6-hour TTL).
- Subsequent visits within the TTL skip the form.
Passwords are stored as bcrypt hashes and never transmitted back to you — they're hashed on save.
On the edit form, leave the password field empty to keep the current one; tick clear password to remove protection.
Expiry
Optional. After the expiry datetime passes, visits return HTTP 410 Gone with a "link unavailable" page. Analytics for visits that already happened are preserved.
QR codes
Every link gets a QR code at /{code}/qr. Default is 256×256 PNG; override with ?size=N (clamped to 64–1024). Suitable for printing — 1024×1024 looks clean at poster scale.
Analytics
Click Stats on any link row. Shows:
- Total visits — every recorded redirect
- Unique IPs — distinct visitor IPs
- Last 7 days — count in the last week
- Crawler share — percentage of visits detected as bots (Googlebot, Bingbot, etc.)
Breakdowns below the KPIs show platform / browser / device / top referrers. "Direct" means no Referer header was sent. Tail beyond the top 8 is collapsed into "Other."
Visits are recorded asynchronously (fire-and-forget goroutine) so the redirect isn't blocked waiting on the DB write.
Archive
Archiving a link keeps the row around (and preserves visit history) but makes the code return "link unavailable" (HTTP 410) to visitors. Admins can archive any link from /admin/links. Unarchive restores visitor access.
Encrypted notes
One-time end-to-end encrypted messages. The server stores ciphertext only; the decryption key lives in the URL fragment, which browsers never send over the wire.
How they work
- Your browser generates a random 32-byte AES-GCM key and 12-byte nonce.
- It encrypts the message locally using
crypto.subtle.encrypt. Plaintext never leaves your device. - POST
{iv, ciphertext}to the server. Server stores the row. - Browser constructs the URL as
https://textdrop.co/p/<code>#k=<key>. The key is in the fragment — HTTP clients never include fragments in requests. - Share the URL. Recipient's browser fetches the ciphertext, pulls the key from the fragment, decrypts locally.
Fields
- Message
- Supports Markdown (bold, italic, headings, lists, links, inline code, fenced code blocks). Rendered with marked + DOMPurify so senders can't embed
<script>orjavascript:URLs. External links open in a new tab. - Max views
- Total number of times the note can be opened before the server deletes the row.
1(default) is classic one-time: the first person to open it reads it, then it's gone for everyone. The counter increments on every fetch, even if the decrypt fails — so a lost key still burns a view (intentional, prevents offline brute force). - Expires
- Optional cutoff. If unset, the default is 7 days from creation. After expiry the row is deleted on next fetch, before any ciphertext is returned.
Threat caveats
This is zero-knowledge, not trustless. The important limitations:
- Server serves the JS. A compromised server could return booby-trapped JS on a specific visit that exfiltrates the decryption key. Same limitation applies to PrivateBin, SendSafely, 1Password share links, etc. Mitigation requires things like Subresource Integrity, code transparency logs, or signed builds — not implemented here.
- URL leaks = key leaks. Browser history, corporate MITM proxies, and re-shortening services can all capture the fragment. Use incognito + don't re-shorten the URL.
- Metadata is observable. Server sees who created it, when, visit IPs, view count, and ciphertext length (which approximates plaintext length).
Short version: we're a ciphertext escrow service with a view counter. The secret lives in the URL the creator hands to the recipient; we never touch it.
Account
Register
Email + password (8–200 chars) + name. A verification email is sent via Postmark from info@textdrop.co; login is blocked until the link is clicked. Verify tokens are valid for 48 hours, single-use. The only information Textdrop stores about you is your email, display name, bcrypt password hash, and the links + notes you create.
Login + sessions
Sessions are server-side row-backed (not JWTs), keyed by a 32-byte opaque cookie. TTL is 30 days and slides forward on every authenticated request, so active users stay logged in indefinitely. Logout, password reset, or admin account-disable immediately invalidate the session.
Password reset
Submit your email at /auth/forgot, get a reset link. Token is valid 60 minutes and single-use. Requesting a new reset supersedes any outstanding token for your account. On a successful password change, all of your active sessions are revoked (defense against stolen session + lost-then-recovered account).
Rate limits
Protects against brute-force and inbox spam. All limits are per client IP, in-memory, fixed-window:
| Endpoint | Limit | Window |
|---|---|---|
POST /auth/register | 5 | 1 hour |
POST /auth/login | 10 | 15 min |
POST /auth/forgot | 3 | 1 hour |
POST /auth/reset | 20 | 1 hour |
POST /{code} (visit password) | 10 per (IP, code) | 15 min |
On exceed, server returns 429 Too Many Requests with a Retry-After header telling you how many seconds until the window resets.
Admin
Users with is_admin=1 see an "Admin" link in the topbar. Admin surfaces:
- /admin — KPI overview: total users, admins, links, visits + recent signups
- /admin/users — all users, with status, verified flag, link counts, join date
- /admin/users/{id} — detail page with per-user actions: toggle admin, disable/re-enable (nukes their sessions immediately on disable), re-send verify email
- /admin/links — every link across every user, with Created + Last visit timestamps, archive/unarchive controls
Safety rails:
- Last-admin guard — you can't revoke admin from the only remaining admin
- You can't disable your own account — another admin must do it
- Admin toggles self-view hides destructive buttons so nobody clicks them expecting them to work
HTTP API
Minimal JSON API for programmatic short-link creation. Currently anonymous-only (no auth token scheme yet); authenticated callers get user_id attached if their session cookie is sent.
POST /api/shorten
curl -X POST https://textdrop.co/api/shorten \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/long/path","code":"myalias","password":"optional","expires_at":"2027-01-01T00:00:00Z"}'
Response:
{"code":"myalias","short_url":"https://textdrop.co/myalias","url":"https://example.com/long/path","created_at":"2026-04-25T00:00:00Z"}
Errors: 400 (bad body / invalid URL / expires_at not RFC3339), 409 (alias taken), 413 (URL > 2048 chars), 500.