We just open-sourced a complete content platform built on SatsRail's Lightning payment infrastructure. It's a Next.js app with AES-256-GCM encryption, macaroon-based access tokens, and a payment flow where the server never sees the content it delivers. Anyone can fork it, brand it, deploy it. But the part worth talking about is a design decision we made early: the merchant can opt for blind payments — processing sales without ever learning who the buyer is or what they bought.
The repo is on GitHub. MIT license. Docker setup, setup wizard, runs in minutes. This post is about the architecture underneath it and what happens when you give a merchant the option to not know.
The Architecture: What Knows What
The media app is a client of SatsRail's API. It doesn't process payments itself — it calls SatsRail endpoints for product creation, checkout sessions, and key delivery. This separation is the foundation of everything that follows.
Here's how the layers break down:
| Layer | Stores | Serves to Viewers | Cannot Access |
|---|---|---|---|
| SatsRail (payment rail) | Product name, price, SKU. AES-256-GCM encryption keys. Invoice records. | Decryption key + macaroon (only after confirmed payment). | Content descriptions, categories, or metadata. Buyer identity. What the content actually is. |
| Media App / MongoDB (content layer) | Channels, media metadata, categories. Plaintext content URLs (admin-only). Encrypted content blobs. Customer nicknames (if accounts enabled). | Encrypted blobs only. Plaintext URLs never appear in viewer-facing responses. | Encryption keys (held by SatsRail, never stored in MongoDB). Customer real identity. |
| Browser (client) | Macaroon in localStorage. Decrypted content URL (in memory only). | N/A — endpoint, not a server. | Nothing persists beyond localStorage. Close the tab, clear the cache, it's gone. |
This isn't a policy. It's a structural constraint. SatsRail stores product records and encryption keys but has no concept of content categories or buyer identity. MongoDB stores the plaintext content URLs and media metadata, but viewer-facing pages only ever receive the encrypted blob — the decryption key lives on SatsRail and is only delivered to the browser after payment. The browser decrypts client-side and never sends the plaintext URL back to any server.
No single layer in the stack has a complete picture of the transaction.
How Blind Payments Work
Lightning payments are natively pseudonymous. There's no "billing address" field on a Lightning invoice. The buyer scans a QR code, their wallet routes the payment through the Lightning Network, and sats arrive in the merchant's wallet. The merchant's node sees an incoming payment from a routing node — not from the buyer.
The media app builds on this by making buyer identity collection optional by design:
- Pseudonymous accounts — Customer signup requires a nickname and password. No email. No phone number. No real name. The "account" exists only to track purchase history and favorites within that instance. The merchant can disable accounts entirely and allow pure anonymous purchases.
- No payment identity linkage — When a customer pays a Lightning invoice, SatsRail confirms the payment and returns a macaroon. The macaroon contains an order ID and product ID — not a customer ID. It's a proof of payment, not a proof of identity.
- Client-side key delivery — The decryption key travels from SatsRail to the browser. The media app server participates in the checkout flow but doesn't need to know who initiated it. The browser stores the macaroon locally for re-access.
- No server-side purchase logs tied to identity — The media app records that a product was sold. In blind mode, it doesn't record to whom. The macaroon in the buyer's browser is the only proof of purchase.
The result: a merchant can sell content where the only record of the transaction is a Lightning payment confirmation on SatsRail's side (no buyer identity) and a macaroon in the buyer's browser (no server-side copy tied to a person).
The Encryption Flow, Step by Step
This is the technical flow for every purchase:
- The operator uploads media to their own CDN (Bunny.net, Vimeo, YouTube unlisted, any URL). The media app stores the plaintext URL in MongoDB, accessible only to admin staff.
- The operator creates a SatsRail product via the API. SatsRail generates a product-specific AES-256-GCM encryption key.
- The media app fetches the key from
GET /api/v1/m/products/:id/key, encrypts the media URL, and stores the encrypted version asencrypted_stream_urlin MongoDB alongside the plaintext. The key is not stored in the media app's database. Viewer-facing pages only ever receive the encrypted blob. - A buyer visits the media page. The encrypted blob is embedded in the HTML. The content is locked.
- The buyer clicks "Buy." The media app calls
POST /api/v1/m/ordersthenPOST /api/v1/checkout/sessions. SatsRail returns a checkout URL with a Lightning invoice. - The buyer pays. SatsRail confirms the payment, generates a macaroon
{ order_id, product_id, expiry }, and returns the decryption key + macaroon to the browser. - The browser decrypts the media URL using Web Crypto API's
AES-GCMmode. The plaintext URL is used to load the content. The decrypted URL never leaves the browser. - The macaroon is stored in
localStorage. On return visits, the browser callsPOST /api/v1/pub/access/verifywith the macaroon. If valid and not expired, SatsRail returns the key again. No re-payment required.
Key rotation is handled via webhooks. When an operator rotates a product key on SatsRail (POST /api/v1/m/products/:id/rotate_key), SatsRail fires a product.key_rotated webhook. The media app receives it, fetches the new key, and re-encrypts all associated media blobs. Existing macaroons continue to work — they trigger key delivery from SatsRail, which always returns the current key.
What "Blind" Means for the Merchant
Traditional content platforms operate on total visibility. The platform sees every upload, every purchase, every buyer-seller relationship. This visibility is treated as a feature — analytics, personalization, fraud detection.
But visibility is also liability.
If the platform sees all content, it can be compelled to moderate it, hand it over in discovery, or produce it under subpoena. If it stores buyer identities, a breach exposes who bought what. If it processes payments through a card network, Visa and Mastercard can dictate what categories are "acceptable."
Blind payments invert this. The merchant chooses not to collect buyer identity. The architecture enforces that the server can't see decrypted content. The payment rail cannot block the transaction because Lightning is non-custodial and final.
Here's what blind mode looks like in practice:
- The merchant knows: That products were sold. Revenue totals. Which products are popular (by sales count). That's it.
- The merchant doesn't know: Who bought each product. Buyer demographics. Purchase patterns tied to individuals. Browsing history.
- SatsRail knows: Invoice amounts. That payments were confirmed. Product IDs. Nothing about content or buyers.
- A breach exposes: Plaintext content URLs (in MongoDB), product metadata (names and prices), and aggregate sales data. No buyer identities. No purchase histories tied to people. No encryption keys (those live on SatsRail).
The Implications Are Structural, Not Philosophical
This isn't about privacy ideology. It's about what happens when the architecture makes certain outcomes impossible rather than merely prohibited by policy.
Government Data Requests
A subpoena can compel a platform to produce what it has. When "what it has" is encrypted blobs it can't decrypt and sales records with no buyer identity attached, compliance is trivial — you hand over everything, and it reveals nothing useful about individual buyers. You can't produce data you never collected.
Payment Processor Censorship
Visa and Mastercard have become the most effective content moderators on the internet. They don't need legislation — they just update their acceptable use policies and entire creator categories lose the ability to transact overnight.
Lightning payments route around this completely. There's no processor to file a chargeback with. No underwriting committee reviewing your business model. No quarterly risk assessment that might shut you down. The payment moves from buyer wallet to seller wallet through the Lightning Network. The only intermediary is math.
Data Breach Economics
The value of a data breach is proportional to the sensitivity of what's stored. A breach of a traditional content platform exposes names, emails, payment card tokens, purchase histories — devastating for buyers of sensitive content. A breach of a blind-payment media app instance exposes encrypted content blobs and anonymous sales records. The attack surface exists, but the reward for an attacker is near zero.
Creator Independence
The media app supports two creator modes. Managed creators have their content handled by the platform operator. Independent creators get their own SatsRail API keys — payments go directly to their wallet, not through the operator. In independent mode with blind payments, the creator receives sats the moment a buyer pays, the operator processes the encrypted delivery, and nobody in the chain knows both who paid and what they got.
What This Doesn't Solve
Honesty about limitations matters more than marketing claims.
- Plaintext URLs exist in MongoDB. The media app stores the original source URLs in the database for admin access and re-encryption. Viewer-facing pages only receive the encrypted blob, and the decryption key only arrives after payment. But if the MongoDB instance itself is compromised, an attacker gets the plaintext URLs. Whether those URLs are useful depends on the hosting platform — Bunny.net private buckets require additional auth, YouTube unlisted URLs are directly playable. The encryption protects content as served to viewers, not at rest in the admin database.
- Pseudonymous isn't anonymous. A customer's nickname is pseudonymous. If they reuse it across platforms, it's correlatable. If they access the platform without a VPN, their IP is logged by the web server. The architecture doesn't solve network-layer anonymity.
- Lightning routing isn't perfectly private. Lightning payments route through intermediate nodes. A sophisticated network observer with visibility into multiple routing nodes could potentially correlate payments. This is a known Lightning privacy limitation, not specific to this app.
- The operator still controls the platform. They choose what categories exist, what content is uploaded, and who gets a channel. Blind payments mean the operator doesn't know who buys — not that the operator has no editorial control over what's offered.
The Stack
For developers evaluating a fork:
| Component | Technology | Why |
|---|---|---|
| Framework | Next.js 15, App Router | Server components for encrypted blob delivery, API routes for checkout flow |
| Database | MongoDB + Mongoose | Document model fits content + channel schema; works with Atlas or self-hosted |
| Encryption | AES-256-GCM via Web Crypto API | Browser-native, no dependencies, military-grade, authenticated encryption |
| Auth | NextAuth.js v5 | Credentials provider for staff (SatsRail-linked) and customers (local MongoDB) |
| Styling | Tailwind CSS v4 | CSS variables for white-label theming per instance |
| Payments | SatsRail API | Non-custodial Lightning; product keys, checkout sessions, macaroon verification |
| Access Control | Macaroons (HMAC-SHA256) | Signed, expirable, verifiable without storing session state server-side |
| Deployment | Docker / Vercel / bare metal | docker compose up -d gets you running in minutes |
TypeScript throughout, strict mode. The codebase is clean enough to fork and extend without having to rewrite the payment or encryption layers.
Why Open Source This
SatsRail is payment infrastructure. Our business model is API subscriptions, not content platform revenue. The more platforms that deploy this app and consume SatsRail's API, the more the infrastructure grows.
Open-sourcing the media app does something else: it makes the architecture auditable. Anyone can read the encryption flow, verify that the server never sees decrypted content, confirm that buyer identity is structurally excluded from the data model. "Trust us" is not a privacy architecture. "Read the code" is.
The repo is at github.com/SatsRail/media. MIT license. Clone it, deploy it, break it apart, build something better. The only thing it needs from SatsRail is an API key for the payment rail.
The rest is yours.
Read more about the media app on satsrail.com/media-app, or get your API key to start building.