readings in spaces
Files
README.md

recite #

A private reading desk. Paste a passage, choose a provider, model, and voice, then listen to saved readings on your phone.

The frontend lives on Wisp. A Cloudflare Worker serves it alongside OAuth and audio routes at recite.waow.tech, keeping authentication first-party on Safari. Access is restricted to the verified DIDs of nate.spaces-alpha.bsky.network and toochaotic.bsky.social.

Voice catalog #

GET /api/catalog returns the configured providers, their models, compatible voices, text limits, and delivery capabilities. The UI builds its choices from this response. The server validates the complete selection before making a paid request. Each saved reading retains its provider, model, and voice, including after a model is retired.

Provider adapters live in src/providers/. Add an adapter with its catalog, credential availability check, and MP3 generation function, then register it in src/providers/index.ts. The composer, generation route, and storage do not need provider-specific branches.

The initial adapter is OpenAI, configured with OPENAI_API_KEY. Its catalog offers GPT-4o mini TTS, TTS-1, and TTS-1 HD. Voices and delivery support follow the provider documentation. This is the currently installed adapter, not an application-wide model or voice constraint. Old recordings that predate the provider field remain readable.

Sign-in and private storage #

The server checks the DID allowlist before authorization and on every authenticated request. Following Lore's grant check, completing OAuth alone does not admit an account: the returned token must have the owner-specific space grant, and private-space creation or access must succeed before an app session is issued. Unusable OAuth sessions are revoked. Old sessions without this verification must sign in again. The authenticated startup check repeats the space check before rendering the desk.

An unauthenticated PDS probe is not reliable: both supporting and non-supporting PDSes can return AuthMissing. Accounts can therefore reach their PDS consent screen, but cannot enter recite without usable spaces. Allowlisted accounts without spaces are rejected too.

Each session writes exclusively to its owner's tech.waow.reciteSpace/self space with member-list read and write policies. Text, settings, and MP3 references are tech.waow.reciteTake records. Audio plays through an authenticated proxy. There is no public-storage fallback. Publishing to plyr.fm is a separate action.

There is one generation at a time per account, at most 30 attempts per hour, and an app-wide 4,000-character passage limit. Request IDs prevent duplicate charging for the same request. A failed attempt is never automatically repeated.

Development #

bun install
bunx wrangler d1 migrations apply recite --local
bun run dev:api
bun run dev

Open http://127.0.0.1:45327. Supply provider credentials through the process environment or ignored .dev.vars. Production credentials derive from the canonical sops store. D1 holds OAuth state and sessions; the browser receives only an opaque HttpOnly cookie.

bun run check
bun run test
bun run build

Tests use the real Worker and D1 in Miniflare and exercise access denial, forged cookies, removed allowlist entries, unverified legacy sessions, origin checks, callback binding, catalog compatibility, grant validation, and saved-record compatibility. No network substitutes are used.

Development-only visual fixtures are available at ?preview=empty, ?preview=populated, and ?preview=error. These render the real UI with labeled preview data, cannot generate or change recordings, and are removed from production bundles. They verify layout independently of an authenticated PDS session; they are not end-to-end storage tests.

Deployment #

Commit and push first, then deploy the Worker and publish dist/ to the recite Wisp site. Lexicons are published from waow.tech, resolved through _lexicon.waow.tech.

bunx wrangler d1 migrations apply recite --remote
bun run deploy:api
bun run deploy:site

The original zzstoatzz.io PDS returned HTTP 501 for authenticated space creation; Nate's allowed account is now the spaces-alpha account. Full OAuth callback, generation, and playback against that account require its authenticated session and must not be inferred from a successful authorization redirect.