Textdrop docs

Reference for every feature. Short read. If you're looking at a specific form field, each one deep-links here.

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:

Leave empty to get a random 5-char base62 code.

Visit password

Gates the redirect behind a password. When a protected link is visited:

  1. A password form renders instead of the 302.
  2. On correct entry, the server sets a signed unlock cookie scoped to that specific code (6-hour TTL).
  3. 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:

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

  1. Your browser generates a random 32-byte AES-GCM key and 12-byte nonce.
  2. It encrypts the message locally using crypto.subtle.encrypt. Plaintext never leaves your device.
  3. POST {iv, ciphertext} to the server. Server stores the row.
  4. 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.
  5. 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> or javascript: 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:

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:

EndpointLimitWindow
POST /auth/register51 hour
POST /auth/login1015 min
POST /auth/forgot31 hour
POST /auth/reset201 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:

Safety rails:

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.