← Portfolio

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

Next.js 15 · TypeScript · Postgres · Prisma · Web Crypto API · MinIO/R2 · Stripe

Pre-launch  ·  All 10 milestones code-complete · 86 unit tests · 32/32 Playwright tests


Sound on Paper — paper tapes

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

It should not feel like


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


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, 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.


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
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)

Architecture

Route structure

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

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
    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)

Prisma models


Security Decisions


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