Documentation
How SINGENOTE works
Paste a secret. Share the link. It burns after one read. Encrypted in your browser — the server only ever stores ciphertext.
Overview
SINGENOTE is a one-time note service. You paste text, your browser encrypts it, and you get a link. The first person to open the link and press Open & burn reads the note; at that moment the server deletes it. Anyone who opens the link afterwards sees “This note is gone.”
There are no accounts, no cookies and no wallet needed for the basic flow. Notes that nobody opens expire on their own after 1 hour or 24 hours (7 days for holders).
How it works
- Seal. Your browser generates a random 256-bit key and a 96-bit IV with
crypto.getRandomValues, derives an AES-256-GCM key and encrypts the text. Only the ciphertext, the IV and the expiry are uploaded (plus, for notes with a passphrase, a random salt and a verifier that cannot decrypt anything). - Share. The link looks like
https://singenote.pro/n/<id>#<key>. Everything after#is the URL fragment, which browsers never send to servers. The server knows the note id; it never sees the key. - Burn. Opening the page only reads metadata (does the note exist, does it need a passphrase). The note is consumed by a
POSTsent when the reader presses the button. The server selects the row, checks the passphrase verifier if there is one, and deletes the row in a single SQLite transaction, returning the ciphertext to exactly one caller. The reader's browser decrypts it locally.
Link previews can't burn a note: unfurl bots fetch the page with GET, and GET never deletes anything.
Security model
All binary values are base64url without padding. Format version v: 1.
| Value | Size | How it's made |
|---|---|---|
K_link | 32 bytes | Random. Lives only in the link fragment. |
iv | 12 bytes | Random per note. |
salt | 16 bytes | Random, only when a passphrase is set. |
K_pass | 32 bytes | PBKDF2-HMAC-SHA-256(passphrase NFC, salt, 300,000 iterations). |
K_enc | 32 bytes | HKDF-SHA-256(K_link ‖ K_pass?, salt or 16 zero bytes, info singenote/v1/enc). |
verifier | 32 bytes | Passphrase notes only: HKDF-SHA-256(K_pass, salt, info singenote/v1/check). |
ct | text + 16 bytes | AES-256-GCM(K_enc, iv, AAD singenote/v1, 128-bit tag) over the UTF-8 text. |
Why a wrong passphrase doesn't burn the note
With a passphrase, the reader's browser derives a verifier and sends it with the open request. The server compares it in constant time inside the same transaction as the delete. A mismatch returns 403 and the note stays intact. Five wrong attempts lock that note for 10 minutes.
The verifier is derived from the passphrase only, with a different HKDF label than the encryption key, and contains no bits of K_link. Even someone who reads the database and guesses the passphrase still can't decrypt the note without the link.
Passphrase links carry the salt next to the key: #<key>.<salt>. The salt is not secret; it lets the reader's browser build the verifier before anything is deleted.
Tampered links
If the key in the fragment is altered by hand, the server can't tell — it hands over the ciphertext and deletes it, and decryption fails with “Couldn't decrypt. The link key is wrong, and the note has already been burned.” This is the only path where a note is lost without being read.
What this does NOT protect against
- The recipient. Whoever opens the note can screenshot it, copy it or photograph the screen.
- Compromised devices. Malware or a malicious extension on the sender's or recipient's device can read the text before encryption or after decryption.
- Weak passphrases. The verifier lets anyone who reads the server database (including the operator) brute-force a weak passphrase offline. The note stays encrypted without
K_link, but a passphrase you reuse elsewhere could be guessed. Use a random passphrase of 12+ characters. - Links in chats that keep history. Chat apps and email store the link. If someone else opens it first, the recipient sees “gone” — that tells you it was read, but it was read.
- A malicious operator. This is a web app. A compromised server could serve different JavaScript and steal keys as the page loads. Strict CSP and self-hosted assets reduce the surface; they don't remove it.
- No independent audit. The cryptography uses standard WebCrypto primitives, but the code has not been independently audited.
Limits & tiers
| Tier | Text size | Expiry | Needs |
|---|---|---|---|
| Free | 1,024 bytes (UTF-8) | 1 hour or 24 hours | Nothing |
| Holder | 10,240 bytes (UTF-8) | 1 hour, 24 hours or 7 days | ≥ 500,000 tokens, message signature |
Sizes count bytes, not characters: emoji and non-Latin scripts use more than one byte each. Rate limits per IP: 20 new notes per 10 minutes, 120 metadata checks and 60 open attempts per 10 minutes.
API
The API only ever handles ciphertext. You can script it, but the encryption must happen on your side with the format above — sending plaintext here would defeat the point.
# create (ciphertext only)
curl -s https://singenote.pro/api/notes \
-H 'Content-Type: application/json' \
-d '{"v":1,"ct":"<base64url>","iv":"<16 chars>","ttl":86400,"salt":null,"verifier":null}'
# → 201 {"id":"<22 chars>","expiresAt":"…","tier":"free"}
# metadata (never deletes)
curl -s https://singenote.pro/api/notes/<id>
# → 200 {"exists":true,"hasPassphrase":false,"expiresAt":"…"} · 404 when gone
# open & burn (returns ciphertext once, then deletes)
curl -s -X POST https://singenote.pro/api/notes/<id>/open \
-H 'Content-Type: application/json' -d '{}'
# → 200 {"v":1,"ct":"…","iv":"…","salt":null} · 410 {"error":"gone"}
Errors are always {"error":"<code>","message":"…"}. Wrong passphrase: 403 bad_passphrase with remaining. Locked: 429 locked with retryAfterSec. Live counters: GET /api/stats (a server counter, not an on-chain figure).
Privacy
- No accounts. No cookies. The only thing stored in your browser is an optional holder session (token, wallet address and expiry) in
sessionStorage, cleared when the tab closes. - No access logs on note pages or the API. The web server's access log is off for the whole site. The app logs start-up, errors (without ids, IPs or addresses) and an hourly count of expired notes.
- No third-party requests. Fonts, scripts and images are served from singenote.pro. The Content-Security-Policy only allows this origin. Check the Network tab.
- Rate limits live in memory. They count requests per IP for 10 minutes and are never written to disk.
- No referrer. Pages send
Referrer-Policy: no-referrer, and the reader page replaces its URL with/n/openedafter decryption so the key leaves the tab's history.
Holder verification
After launch, the Holder tier panel asks your wallet to sign a short plain-text message (EIP-191 personal_sign). It is not a transaction, costs no gas and approves nothing. The server recovers your address, reads your token balance on Robinhood Chain with balanceOf, and if you hold at least 500,000 tokens returns a session token valid for one hour.
Your address is never written to the database and is never stored alongside notes. Smart-contract wallets (EIP-1271) aren't supported in v1.
Holding unlocks bigger and longer notes. No staking, no rewards, no promises.
Token
contract address posted at launchThe token trades on Pons on Robinhood Chain. The only on-chain feature in SINGENOTE is the holder balance check above; notes are never stored on-chain.
Changelog
- v1 · 1 Nov 2026 (UTC) — One-time notes with browser-side AES-256-GCM, optional passphrase with server-side verifier, 1 hour / 24 hours expiry, holder tier.