Technical Documentation
Sound On Paper
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
Private beta live · All 10 milestones implemented · 125 unit tests passing · 12 focused Playwright scenarios
The Idea
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.
What It Should Feel Like
It should feel like
- a message passed by hand
- a small art object
- a secret song container
- a minimalist music player
- a physical-world sound object
It should not feel like
- a promo landing page
- a streaming smart link
- a social profile
- a CRM or marketing dashboard
- a generic QR generator
Privacy Architecture
The privacy model is architectural. The server hosts encrypted sound objects and is deliberately designed so it cannot listen to them.
Client-side encryption
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.
URL fragment key
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.
Dynamic QR
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.
Key management
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.
Practical limits
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 Experience
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.
Playback pipeline
- Listener browser shows staged loading messages: "Opening…", "Preparing sound…", "Almost ready…"
- Fetches encrypted Piece manifest by slug
- Reads share key from URL fragment — never sent to server
- Decrypts manifest locally with Web Crypto
- Fetches encrypted audio blob from object storage
- Decrypts full audio blob in browser
- Creates temporary object URL and plays via HTML audio element
- In-flight fetches use AbortController for clean teardown
The Creator Experience
The creator side now opens with a derived three-step path: add audio, make a Piece, then print and test the QR. Each step links directly to the relevant tool and updates from the creator's actual library state, replacing a dashboard that previously showed counts without helping someone decide what to do next.
- Upload MP3 or M4A files up to 50 MB and three minutes, with storage quotas enforced separately
- Record in the browser, trim against a simple waveform, encode to MP3, rename, and save encrypted audio
- Choose Fixed Pieces for permanent paper objects or Dynamic Pieces for changeable, controlled playback
- Schedule multiple encrypted recordings in a Dynamic Queue without changing the printed QR
- Use actionable empty, loading, error, retry, and success states across Audio, Pieces, and Print
- Work from responsive creator views with semantic alerts and keyboard-capable recording trim controls
Build Status
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, browser recording, MP3/M4A validation, 3 min/50 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 | Playwright accepted | Abuse reports, DMCA/counter-notice intake, searchable admin queues, linked records, enforcement logging |
| 8 | Access Controls | Playwright accepted | Expiration, play limits, Pass-required Pieces, one-time and expiring Passes |
| 9 | Billing and Entitlements | Implemented | Stripe integration and plan enforcement are complete; checkout is intentionally disabled during the free private beta |
| 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 |
The current verification set contains 125 passing Vitest unit tests and 12 focused Playwright browser scenarios across setup, encrypted audio, browser recording, Pieces, listener playback, administration, access controls, printing, and private-beta security. GitHub Actions runs Node 22 installation, Prisma generation, lint, type checking, unit tests, and a production build. Stripe checkout remains outside the beta acceptance path because production billing is deliberately disabled.
In summary: Auth.js v5 with PBKDF2 key derivation and IndexedDB non-extractable trusted-device keys; encrypted upload and recording 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; abuse reporting and linked admin moderation queues; expiration, play limits, and Pass-gated listening; optional billing and plan enforcement; and printable PDF, card, and sticker layouts for physical QR artifacts.
Private Beta Readiness
Sound on Paper is now running as a free, invitation-only concierge beta. New creators request access, receive a 24-hour single-use invitation by email, and begin on the Free plan. Artist access is granted manually so the beta can focus on whether people can make and physically scan a Piece without turning onboarding into a checkout funnel.
- Resend-powered invitations with hashed, expiring, single-use tokens and an administrative resend flow
- Password recovery with hashed 60-minute tokens and immediate invalidation of existing sessions
- Clear separation between account recovery and the recovery words needed to unlock encrypted work on a new device
- Database-backed active-user checks so suspending an account also rejects sessions that were already signed in
- Durable Postgres rate limits keyed by HMAC-digested identifiers rather than stored raw IP addresses
- Five-minute signed listening grants after Pass redemption, preventing reloads from consuming multiple uses
- Object-storage verification before database records are created, plus bounded public request schemas
- Production CSP, HSTS, frame denial, MIME protection, privacy policies, and redacted errors with request IDs
- Transactional user and enforcement changes with self-protection, last-admin protection, and audit records
- Rotating authenticator-app codes and single-use recovery codes protecting every administrator session
Tech Stack
| 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 · invite-only signup · administrator TOTP MFA |
| Billing | Stripe checkout, portal, and webhook · disabled mode for complimentary beta access |
| Resend · verified sender · server-only invitation and password recovery adapter | |
| QR generation | SVG and PNG export · error correction level H for sticker layout |
| Unit tests | Vitest · 125 passing |
| Acceptance tests | Playwright · 12 focused browser scenarios against local Postgres and MinIO |
| Runtime target | Node 22 (.nvmrc + package.json engines) |
Architecture
Route structure
app/ (auth)/ signup, login, forgot/reset password, setup, unlock (creator)/ dashboard, audio, pieces, print, settings (listener)/p/ [slug] — public listener route (legal)/ copyright, privacy, terms, DMCA, counter-notice admin/ users, beta, reports, copyright, enforcement admin-mfa/ authenticator enrollment api/ auth, key-vault, audio, pieces, public, admin, legal
Source structure
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
pass-grant.ts short-lived signed grant after Pass redemption
email.ts server-only invitation and password recovery email
rate-limit.ts durable HMAC-keyed Postgres rate limiting
admin-mfa.ts encrypted TOTP enrollment and recovery-code handling
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, 50 MB, 3 min)
Prisma models
- User · CreatorProfile · KeyVault
- AudioAsset — plaintext: mimeType, sizeBytes; encrypted: name, durationMs
- Piece — publicSlug, encryptedManifestBlob, encryptedPieceKeyForCreator, status, access controls
- Pass — pieceId, code, maxUses, usesCount, expiresAt, status
- PieceEvent — scan, play, play_complete, download; no raw IP stored
- AbuseReport · CopyrightNotice · EnforcementAction
- Subscription — plan, status, Stripe provider fields
- BetaAccessRequest · PasswordResetToken · RateLimitBucket
- AdminMfaConfiguration · AdminMfaRecoveryCode
Security Decisions
- Audio encrypted with AES-GCM before leaving the browser — server never sees plaintext
- Recovery phrase never sent to server — PBKDF2 derivation is browser-only
-
Trusted-device keys stored as non-extractable
CryptoKeyobjects in IndexedDB — cannot be exported or read by JavaScript after creation - Share key kept in URL fragment — not sent in HTTP requests, not logged server-side
- Two-phase setup flow — local trusted-device key is verified before the server vault is written, preventing an unrecoverable first-run state
- Pass consumption uses an atomic increment, then a signed HttpOnly grant allows safe playback retries
-
importShareKeyvalidates decoded key is exactly 32 bytes before passing to Web Crypto — deterministic error for malformed keys - Every creator page and API checks current database status, so suspended existing sessions are rejected
- Administrator access requires a separate rotating authenticator step after email and password
- Password reset increments session version, invalidating existing sessions without emailing encryption material
- Rate-limit identifiers are HMAC digests; raw IP addresses are not stored in rate-limit records
- Non-active Piece IDs injected into print preview query string are rejected server-side
Moderation and Safety
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.
Product Guardrails
- Do not turn this into a link-in-bio product
- Do not add external music service links in the MVP
- Do not upload plaintext audio to the server
- Do not store plaintext titles, messages, artwork, or audio
- Do not use magic-link-only login
- Do not add ads or social mechanics to listener pages
- Do not add per-upload copyright checkboxes
- Do not promise guaranteed autoplay
- Do not add proactive copyright scanning of encrypted audio