S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# A1 Session 10 — Public Ledger Profiles v1
|
||||
|
||||
Stage-3 seed (NORTH STAR): every bettor gets a CLV-verified public record —
|
||||
Strava for betting. v1 is the minimum honest version: a handle, one explicit
|
||||
publish toggle, and the user's ENTIRE settled record on a public page.
|
||||
|
||||
## Operating rules
|
||||
- Zero out-of-pocket. Existing Supabase + Express + Next only.
|
||||
- PRIVATE BY DEFAULT. `published` defaults false; publishing is one explicit
|
||||
toggle with copy that says exactly what it does.
|
||||
- Same n-gating as the model record: no percentage under 20 settles
|
||||
(`ledgerService.MIN_AGG_SAMPLE`).
|
||||
- Data semantics: only real settled ledger rows. Nothing curated — a public
|
||||
profile shows ALL settled reads including misses. That is the point.
|
||||
- No existence leak: unknown handle and unpublished handle return the SAME
|
||||
404 body.
|
||||
|
||||
## Data
|
||||
Migration `supabase/migrations/022_public_profiles.sql` (committed, NOT
|
||||
applied — the founder applies migrations):
|
||||
|
||||
```
|
||||
public_profiles (
|
||||
user_id uuid PK REFERENCES auth.users ON DELETE CASCADE,
|
||||
handle text UNIQUE NOT NULL CHECK (handle ~ '^[a-z0-9_]{3,20}$'),
|
||||
published boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
```
|
||||
RLS: anon + authenticated SELECT where `published = true`; owner SELECTs own
|
||||
row; NO client write policies — all writes via the service role.
|
||||
|
||||
## Endpoints (`src/routes/profiles.js`, mounted at /api/profiles)
|
||||
- `GET /api/profiles/me` (requireAuth) → `{ profile: {handle, published,
|
||||
created_at} | null }`.
|
||||
- `POST /api/profiles/me` (requireAuth) → upsert own `{ handle, published }`.
|
||||
Handle validated against `^[a-z0-9_]{3,20}$` (400 on invalid); a handle
|
||||
owned by another user → 409. Service-role write.
|
||||
- `GET /api/profiles/:handle` (public, 60/min) → published profile:
|
||||
`{ handle, aggregate, entries, min_sample }` where
|
||||
- `aggregate` = `ledgerService.getModelAggregate({ userId })` — a new
|
||||
`userId` option swaps the `.is('user_id', null)` public scoping for
|
||||
`.eq('user_id', uid)`. The public default is untouched.
|
||||
- `entries` = that user's SETTLED rows, newest 50, same columns as
|
||||
/api/ledger.
|
||||
Unknown OR unpublished → 404 with the identical body (no existence leak).
|
||||
|
||||
Next proxies: `web/src/app/api/profiles/me/route.ts` (GET/POST, forwards
|
||||
Authorization) and `web/src/app/api/profiles/[handle]/route.ts` (GET).
|
||||
|
||||
## Frontend
|
||||
- `/u/[handle]` — PUBLIC (never gated). Server component shell with metadata
|
||||
`CLV-verified record — @handle · VYNDR` + client content: record header
|
||||
(hit% + beat-close% at n≥20, else RECORD BUILDING), settled rows with
|
||||
outcome + CLV chips (ledger card patterns), OG card via
|
||||
`opengraph-image.tsx` in the segment (Node runtime — NEVER edge, S53 rule).
|
||||
- `/settings` PUBLIC PROFILE section: claim handle + publish toggle. Copy
|
||||
makes public-by-choice explicit: "Publishing puts your ENTIRE settled
|
||||
record on a public page — wins and misses. Private by default."
|
||||
|
||||
## Acceptance criteria
|
||||
1. Migration committed, unapplied, documented in the report.
|
||||
2. `getModelAggregate` default scope unchanged; `userId` scope unit-tested
|
||||
both ways.
|
||||
3. Unpublished and unknown handles return byte-identical 404 bodies.
|
||||
4. Handle regex enforced at API AND database CHECK.
|
||||
5. `/u/...` reachable anonymous; aggregate renders no % under 20 settles.
|
||||
6. Full jest suite green; `web` build exit 0.
|
||||
|
||||
## Test plan
|
||||
- `tests/unit/ledgerAggregateScope.test.js` — public vs userId scoping.
|
||||
- `tests/integration/profilesRoutes.test.js` — handle validation, 409 on
|
||||
taken handle, publish flow, 404-no-leak (mock supabase per
|
||||
ledgerRoutes.test.js pattern).
|
||||
- `tests/unit/publicProfilePage.test.js` — page/OG/settings source
|
||||
assertions (metadata string, no edge runtime, RECORD BUILDING state,
|
||||
private-by-default copy, /u stays ungated).
|
||||
Reference in New Issue
Block a user