feat(circulation): Circulation of Love — anonymous, time-bound letter sharing - #81
Merged
Conversation
Phase 1 of the Love Circulation feature.
What lands:
- docs/specs/love-circulation.md: full design, including what the
feature is NOT (locks down scope), data model, user flow, RLS
rationale, moderation requirements, 5 open questions for review.
- supabase/migrations/<ts>_love_circulation.sql:
• love_letters table (author, content ≤500 chars, language,
pseudonym, moderated_status, posted_at, expires_at, archived)
• letter_holdings table (letter_id + holder_id composite PK so
reactions are idempotent)
• circulation_settings table (receive_letters, share_letters,
ttl_days ∈ {7,14,30})
• Hot-path indexes (live current by language; author's own
letters; per-letter hold count)
• Full RLS:
- Authors always see their own letters
- Other users see only passed + live + matching language +
reader opted-in
- Holds enforce: not your own letter, letter live, reader
opted-in
- Service role full access for moderation + TTL cron
Reviewer questions (in spec) before Phase 2:
1. Share via existing JournalEntry or own flow only?
2. Silent reactions to author or noisy?
3. Default TTL: 7/14/30?
4. Drift teaser for non-opted-in users?
5. Cross-language sharing allowed?
Defaults applied in this PR are my best guesses; spec calls them out
so the reviewer can override before edge fns are built.
Token spend this turn: ~6k output (specs are dense but bounded).
Cumulative: ~6k. Estimate for full feature: ~80k.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds circulation_settings, letter_holdings, love_letters rows/inserts/updates to match supabase/migrations/20260613113018. Mirrors the regen Supabase would emit (alphabetical key order, no extra fields). Closes Phase 1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
moderate-letter:
- Validates auth via shared requireAuth helper
- Calls Gemini 2.5 Flash with carefully reviewed moderation prompt
(pass: sad/grief/anger OK; softfail: ambiguous self-harm + PII +
named-person; block: explicit suicidal intent, harassment, sexual,
spam, doxxing)
- Inserts the row server-side with verdict baked in (service role)
so the client cannot bypass moderation
- Fails closed on gateway errors (softfail, never auto-publish)
circulate-letters:
- Flips archived=true on every expired letter; idempotent
- Auth via CIRCULATION_CRON_SECRET (distinct from user JWT — leaked
user token can't trigger archival)
- Returns { archived: count, at: timestamp }
config.toml updated; both fns set verify_jwt = false because they
gate auth internally (matches existing pattern).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the user-facing surface for the Circulation of Love: Data layer - usePseudonym(language, seed): deterministic name from per-language pool, regenerable ONCE per draft (matches spec rule) - useCirculationSettings: opt-in toggles + TTL chooser, upsert-style - useLoveLetters: list + share via moderate-letter edge fn + idempotent hold/unhold Screens - LettersInCirculationScreen: drift feed with "Join the current" CTA for non-opted-in users; tap card → modal with single "Hold this for a moment" reaction - ShareALetterScreen: 500-char textarea + pseudonym chip + softfail/ block note rendering; release calls moderate-letter - CirculationSettingsScreen: receive/share toggles + 7/14/30 TTL Wiring - 3 new JournalSteps: 'circulation-feed' | 'circulation-share' | 'circulation-settings' - useJournal exposes openCirculationFeed/Share/Settings - JournalApp dispatches them - HomeScreen gains a Waves-icon tile that opens the feed Animation class `animate-letter-drift` is referenced on cards but the keyframes ship in Phase 4 — silent no-op until then. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
src/index.css - Adds animate-letter-drift utility and letterDrift @Keyframes — gentle sine-like translate3d + tiny rotation, 12s loop. Respects prefers-reduced-motion via the existing media-query block. docs/launch/lovable-circulation-display.md - Lovable update prompt to elevate the feed from "list of cards" to "current of paper letters drifting past." Spells out hard constraints (no new deps, preserve every testid, bilingual chrome, reduced-motion). - Lists 7 concrete design moves (paper aesthetic, varied animation duration so cards drift past each other, softer Join CTA, more negative space, horizontal sway, opened-letter modal as paper unfolded, ghost- letter hint in HomeScreen tile) — Lovable picks the combination. - The "end goal" the user asked for last conversation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…CHANGELOG Unit (vitest) - src/lib/pseudonyms.test.ts (8): FNV hash determinism + 32-bit range, pool ≥15 entries / no dups per language, in-pool selection, (lang, seed) stability, distinct names for distinct seeds. - src/hooks/usePseudonym.test.ts (3): stable across rerenders, regenerate caps at 1 + canRegenerate flips false, respects language pool. E2E (playwright) - e2e/circulation.spec.ts (5): home tile visible + labelled bilingually, click → feed screen, non-opted-in shows Join CTA + hides Share button, feed→settings navigation, TTL chooser exposes all three durations. Mocks circulation_settings + love_letters REST endpoints (return empty arrays) so hooks don't crash on mount. docs/CHANGELOG.md - New 2026-06-18 section with full Circulation of Love writeup (what it is and is NOT, schema, RLS, edge fns, screens, animation, Lovable prompt, ADHD-friendly rationale). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Inspired by Train's grandfather's idea of the circulation of love. An opt-in, anonymous, time-bound sharing layer where users release a short (≤500 char) letter in their primary language. It rides a 14-day current — visible to other opted-in users in the same primary language — then quietly archives. The only reaction is Hold this for a moment (silent, idempotent, private to the author).
🟡 What it is NOT (locked at spec time): not a social network, not cross-lingual, not public outside the app, not viral, not search-indexed.
What ships (5 phases bundled)
docs/specs/love-circulation.md,supabase/migrations/20260613113018_…sql,types.tsregenmoderate-letter,circulate-lettersusePseudonym,useCirculationSettings,useLoveLetters;LettersInCirculationScreen,ShareALetterScreen,CirculationSettingsScreenanimate-letter-driftkeyframe inindex.css,docs/launch/lovable-circulation-display.mdHard constraints honored
moderated_status != 'pending'(RLS); themoderate-letteredge fn inserts with the verdict baked in using service role. Gateway errors fail closed (softfail, never auto-publish).passed + live + matching language + their own receive_letters = true. Holds require the same gate + non-own letter. Authors always retain access to their own letters (Object Permanence per CLAUDE.md).letter_holdingsPK =(letter_id, holder_id). Tapping twice = 1 hold.code 23505treated as success.circulate-lettersusesCIRCULATION_CRON_SECRET, not a user JWT — leaked user token can't trigger archival.prefers-reduced-motionrespected for the new drift animation.Defaults applied (open questions from the spec)
Marathon-mode build. Applied the spec's recommended defaults:
Override any of these before merge if needed.
Deploy notes
After merge, run these in Supabase:
The migration is the only required infrastructure piece — everything else gracefully degrades (the feed shows the empty/error state if the tables don't yet exist).
Test plan
npm test→ 359 vitest pass (+11 new)npm run test:e2e -- circulation.spec.ts→ 5 specs passnpx tsc --noEmit→ 0 errorsprefers-reduced-motion, confirm letter cards don't driftEnd-goal artifact
docs/launch/lovable-circulation-display.mdis the Lovable update prompt Train asked for last conversation — paste it into Lovable's chat once this PR lands. Hands off the visual elevation (paper texture, varied drift duration, ghost-letter hint behind CTA) to Lovable while keeping the data + hooks + testids frozen.🤖 Generated with Claude Code