Technical Documentation
Put sound into paper — a privacy-first, QR-based audio sharing system built for artists who want to share sound as an intimate physical object
Pre-launch · All 10 milestones code-complete · 86 unit tests · 32/32 Playwright tests
I watch my younger students reaching for analog. They're hunting down digicams, shooting film, buying cassettes. There's something they're after — something physical, personal, and real — that purely digital music sharing has removed. I've wanted to restore a physical dimension to sound for a long time. Not nostalgia exactly, but the feeling that music can be an object you hold, pass to someone, and give as a gift. Sound on Paper is built around that idea. A printed card with a song inside it. Snail mail for sound. The QR code generators already exist — this is the ecosystem, the experience, the thing that's been missing. A companion physical player is in development — the goal is a complete platform, not just a web app.
Control the paper = control the song.
The privacy model is architectural. The server hosts encrypted sound objects and is deliberately designed so it cannot listen to them.
Audio, artwork, title, and message are encrypted in the browser before upload using AES-GCM via the Web Crypto API. The server stores only encrypted files, encrypted metadata, non-sensitive routing metadata, and aggregate event records. It never receives plaintext audio, titles, messages, or artwork.
The QR code points to a URL shaped like:
https://soundonpaper.com/p/quiet-river-17#k=PUBLIC_SHARE_KEY
The slug
quiet-river-17
is sent to the server. The fragment — everything after
#
— is never sent in normal HTTP requests. The browser uses it locally to decrypt the Piece. The server can
serve the encrypted manifest without ever knowing the decryption key.
A printed QR code keeps working even when the creator changes the audio behind it. Each Piece has a stable public share key embedded in the QR at generation time. New audio versions are encrypted so the same printed paper decrypts the current assigned content. The creator's encrypted key set retains the Piece key so updates remain possible indefinitely. A printed QR can be updated to play different audio at any time without reprinting — the paper stays the same.
On account creation, a creator master key is generated in the browser. A recovery phrase — shown exactly
once — can unlock it. Only encrypted key material is stored server-side. Trusted-device keys are stored as
non-extractable
CryptoKey
objects in IndexedDB. If a creator loses their recovery phrase and has no trusted device, the company cannot
recover their encrypted content. This is explained during setup in plain language without making the product
feel like a security bunker.
The user-facing privacy promise is intimate and private — not magically impossible to copy. Client-side encryption prevents the company from reading stored content by default. It does not prevent a listener from recording audio from their device, sharing the QR link, or submitting the key during an abuse report. The product is honest about this distinction.
The listener page shows almost nothing by default: a large play button, an optional progress bar, optional title, optional short message, optional artwork. No creator identity, no promotional links, no social mechanics, no engagement bait.
A quiet response control — present like a small folded corner of paper — can reveal lightweight actions: a heart and a report. It is secondary to listening in every sense. Six themes — Plain Paper, Blank, Night Paper, Signal, Blue Hour, Red Room — provide restrained visual expression that feels like paper stocks, not decorative skins.
The project is built in testable milestones — not because AI can't accelerate coding, but because the product has high-risk promises that must be validated incrementally. Each milestone leaves behind a working slice that is tested before more complexity is added.
| 0 | Repo and App Foundation | Complete | Next.js 15, TypeScript strict, Tailwind v4, Prisma, Vitest, Playwright |
| 1 | Auth and Private Key Setup | Complete | Auth.js v5, PBKDF2 key derivation, IndexedDB non-extractable device keys, recovery phrase flow |
| 2 | Encrypted Audio Upload | Playwright accepted | AES-GCM browser encryption, presigned S3 upload, MP3/M4A validation, 3 min/10 MB limits |
| 3 | Piece Creation and Dynamic QR | Playwright accepted | Encrypted manifest, stable share keys, SVG/PNG QR export, word-word-NN slugs |
| 4 | Listener Playback | Playwright accepted | Browser-side decryption, one-tap playback, graceful errors for missing/invalid/wrong key |
| 5 | Themes and Listener Response | Playwright accepted | Six themes, heart toggle, report flow with key-sharing confirmation |
| 6 | Privacy-Friendly Analytics | Playwright accepted | Scan, play, completion events — no raw IP, no fingerprinting, no third-party scripts |
| 7 | Reporting, Copyright, Enforcement | Code complete | Abuse reports, DMCA/counter-notice intake, admin moderation UI, enforcement logging — acceptance pending admin account setup |
| 8 | Access Controls | Playwright accepted | Expiration, play limits, Pass-required Pieces, one-time and expiring Passes |
| 9 | Billing and Entitlements | Code complete | Stripe checkout/portal/webhook, plan-limit enforcement on Pieces and storage — acceptance pending Stripe test credentials |
| 10 | Print and Physical Artifact Tools | Playwright accepted | Sheet (A4), Card (85×54 mm), and Sticker (60×60 mm) print layouts, PDF export, optional URL beneath QR |
86 unit tests pass. 32 of 32 Playwright acceptance tests pass against local MinIO. Remaining: M7 admin acceptance, M9 Stripe billing acceptance, mobile QR LAN testing.
In summary: Auth.js v5 with PBKDF2 key derivation and IndexedDB non-extractable trusted-device keys; encrypted audio upload to S3-compatible object storage; dynamic QR generation with stable fragment share keys; browser-side decryption with one-tap playback; six listener themes; privacy-friendly analytics with no raw IP retention; abuse reporting and DMCA intake with admin moderation UI; expiration, play limits, and Pass-gated listening; Stripe billing and plan enforcement; and printable PDF, card, and sticker layouts for physical QR artifacts.
| Framework | Next.js 15 App Router |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS v4 |
| Database | Postgres · Prisma ORM |
| Object storage | MinIO (local) · Cloudflare R2 / S3-compatible (production) |
| Browser encryption | Web Crypto API · AES-GCM · PBKDF2 |
| Trusted device keys | IndexedDB · non-extractable AES-KW CryptoKey objects |
| Auth | Auth.js v5 credentials |
| Billing | Stripe checkout, portal, and webhook |
| QR generation | SVG and PNG export · error correction level H for sticker layout |
| Unit tests | Vitest · 86 passing |
| Acceptance tests | Playwright · 32/32 passing against local MinIO |
| Runtime target | Node 22 (.nvmrc + package.json engines) |
app/ (auth)/ signup, login, setup, unlock (creator)/ dashboard, audio, pieces, print, settings (listener)/p/ [slug] — public listener route (legal)/ copyright, privacy, terms, DMCA, counter-notice admin/ reports, copyright, enforcement api/ auth, key-vault, audio, pieces, public, admin, legal
src/
crypto/
browser-crypto.ts key generation, wrapping, unwrapping
key-derivation.ts PBKDF2 derivation from recovery phrase
manifest-crypto.ts Piece manifest encrypt/decrypt
audio-crypto.ts AES-GCM audio encrypt/decrypt
device-key-store.ts IndexedDB non-extractable CryptoKey storage
lib/
access-control.ts expiry, play-limit, pass validation helpers
billing.ts Stripe customer, checkout, portal, webhook
entitlements.ts plan limits and enforcement
events.ts privacy-friendly event ingestion
pass-code.ts WORD-NN pass code generation
qr.ts QR SVG generation for listener URLs and print
slug.ts word-word-NN slug generation
storage.ts S3-compatible object storage abstraction
themes.ts listener theme token definitions
validation.ts locked audio constraints (MP3/M4A, 10 MB, 3 min)
CryptoKey
objects in IndexedDB — cannot be exported or read by JavaScript after creation
importShareKey
validates decoded key is exactly 32 bytes before passing to Web Crypto — deterministic error for
malformed keys
The privacy model creates a real tension with safety. The product does not pretend otherwise. The principle: no surveillance, clear safety boundaries, human review only when someone with access reports abuse.
When a listener files a report, the flow explains that submitting it shares the full Piece URL — including the fragment key — with moderation. This is an explicit, informed action. Moderators can decrypt and review reported Pieces, disable Piece routes, record copyright notices, and log enforcement actions against accounts. DMCA notice and counter-notice intake is built into the legal pages. Repeat-infringer tracking is modeled in the data schema.