Token security model
L402 tokens consist of two parts joined by::
- Macaroon — base64-encoded JSON containing
{hash, exp}. Signed by SHA-256. Cannot be forged without knowing the preimage. - Preimage — the 32-byte secret that, when hashed with SHA-256, must match the hash embedded in the macaroon.
Replay protection
l402-kit includes built-in replay protection. Each preimage can only be used once:401 Token already used.
Default store: in-memory Set. This means:
- Restarts clear the replay store (tokens become reusable across restarts)
- Multiple instances don’t share state
Token expiry
Tokens carry anexp field (Unix timestamp in ms). The SDK rejects expired tokens automatically.
Default TTL: 1 hour (set by the Lightning provider when creating the invoice).
HTTPS is required
Never use L402 over plain HTTP in production. The macaroon and preimage are transmitted in theAuthorization header. Over HTTP, they are exposed to network attackers.
Rate limiting
L402 verifies tokens cryptographically, not by looking up a database — so it’s cheap. But Lightning invoice creation (the 402 response) calls your Lightning provider’s API. Protect invoice creation with a rate limiter to prevent DoS:Secrets management
Never hardcode API keys. Use environment variables:.env + dotenv (never committed to git).
Admin endpoint authentication
If you expose admin or stats endpoints, never accept secrets via URL query parameters. URLs are logged in full by reverse proxies, CDNs, and cloud providers — including the query string.Supabase key hygiene
UseSUPABASE_SERVICE_KEY (server-side only) vs SUPABASE_ANON_KEY (safe to expose to clients) correctly:
| Key | Use | Bypasses RLS? |
|---|---|---|
anon | VS Code extension, browser clients | No |
service_role | Server-side API functions | Yes |
pro_access) must use the service key — never the anon key. RLS policies on sensitive tables should not grant anon access.
Content Security Policy
If you serve a frontend alongside your API, ensure your CSP doesn’t block the L402 flow:Data management — user data deletion
Users can delete all their payment history and Pro subscription from within the VS Code extension at any time (Settings → Danger Zone). The extension calls:- All rows in
paymentswhereowner_address = ? - All rows in
pro_accesswhereaddress = ?
The extension requires the user to type their Lightning address verbatim before the delete button is enabled — GitHub-style confirmation for destructive actions.
Privacy & data minimization
l402-kit is designed to collect the minimum data needed to operate. Here is what is stored, why, and how to harden each field.What gets stored
| Table | Field | Why | Sensitivity |
|---|---|---|---|
payments | preimage | Replay protection + proof of payment | ⚠️ Medium — hash it instead (see below) |
payments | owner_address | Attribute revenue to Lightning address | Low — Lightning addresses are public |
payments | amount_sats | Dashboard stats | Low |
payments | endpoint | Per-endpoint analytics | Low–medium |
pro_access | address | Verify Pro subscription | Low — public |
waitlist | email | Send welcome + release emails | ⚠️ High — real PII, encrypt or omit |
waitlist | lightning_address | Optional identity signal | Low |
Hash preimages instead of storing them raw
The preimage is the 32-byte secret that proves a Lightning payment was made. Storing it raw means a database breach exposes every proof. Store the SHA-256 hash instead — it is already public (embedded in the BOLT11 invoice) and sufficient for replay protection:Protect waitlist emails
Email addresses are the only true PII in the system. Options in order of increasing protection:Prove wallet ownership before deleting data (LNURL-auth)
The/api/delete-data endpoint should only accept requests from the actual owner of the Lightning address. Use LNURL-auth to prove ownership cryptographically — the user signs a server challenge with their Lightning wallet private key, with no password or account required:
Checklist
Before going live
Before going live
- HTTPS enforced on all endpoints
- API keys in environment variables, not source code
- Admin/stats endpoints use header auth, not
?secret=URL params - Supabase
service_rolekey used server-side;anonkey only on clients - Sensitive Supabase tables have no
anonSELECT policy - Data deletion endpoint (
/api/delete-data) uses service key, never anon - Rate limiter on invoice creation
- Replay protection tested (try reusing a preimage → expect 401)
- Token expiry tested (set short TTL in dev, confirm 401 after expiry)
- Preimages stored as SHA-256 hashes, not raw secrets
- Waitlist emails encrypted at rest or discarded after sending
-
/api/delete-datarequires LNURL-auth proof of wallet ownership
For high-traffic APIs
For high-traffic APIs
- Redis-backed replay store (shared across instances)
- Monitoring on 402 response rates (spike = potential DoS)
- Lightning provider failover (BlinkProvider → LNbitsProvider fallback)
- Status page monitoring for your Lightning provider (e.g. status.blink.sv)