A society of the well-rested. Entry is by mark only.
Members join through another member's link, take a pseudonym, and connect the wearable they sleep with (WHOOP or Oura). There is no account: the wearable is the login. Every morning a Telegram bot reads the house — who testified, who cleared the bar, who was silent — under pseudonyms only. The rules live in the Charter.
This is v0: the ledger and the reading. The charter's exile rule is computed and shown, not yet enforced.
/j/<slug> the invitation: "You have been marked by Veiled Heron." [public]
/enter roll a name, pick a device -> OAuth [public]
/api/connect/* OAuth start + callback; creates the member, sets a signed cookie
/me your nights, streak, standing, your own mark, "Receive your summons"
/ledger the week, for those within
/charter the rules, live numbers
/api/cron/digest sync everyone, then post the morning reading to Telegram [cron, 06:00 UTC]
/api/cron/sync sync everyone (Pro plans; Hobby crons run once a day) [cron]
/api/telegram bot webhook: /start <token> links a member, hands out a one-time group invite; /ladder
- Next.js 16 (App Router) on Vercel, Supabase Postgres accessed only from the server with the service role. No client-side database access, so tables carry RLS with no policies.
- Sessions are HMAC-signed cookies holding the member id. Reconnecting an already-known wearable logs you back in instead of creating a duplicate.
- Scores are provider-native. WHOOP
sleep_performance_percentage; Ouradaily_sleep.score. The bar is per device (src/lib/rules.ts). Ranking is by nights cleared, then streak — never by raw score across devices. - Polling, not webhooks. Each sync re-pulls the trailing three nights, which also picks up score revisions.
- All rules are pure functions in
src/lib/rules.tswith tests.
Create a project. Run supabase/migrations/0001_init.sql in the SQL editor. Copy the project URL and the service role key.
Create an app at https://developer.whoop.com. Redirect URI: https://<your-domain>/api/connect/whoop/callback. Scopes: read:sleep read:profile offline.
Create an app at https://cloud.ouraring.com/oauth/applications. Redirect URI: https://<your-domain>/api/connect/oura/callback. Oura returns 403 for members whose Oura subscription has lapsed; the connection is marked lapsed rather than treated as a bad night.
-
Create a bot with @BotFather. Note the token and username.
-
Create the group. Add the bot as an admin with permission to invite users via link.
-
Get the group's chat id (a negative number). Easiest: temporarily add @userinfobot, or call
https://api.telegram.org/bot<token>/getUpdatesafter posting in the group. -
After deploying, register the webhook once:
curl -X POST -H "Authorization: Bearer $CRON_SECRET" https://<your-domain>/api/telegram/setup
cp .env.example .env.local # fill in
pnpm install
pnpm devOn Vercel, set every variable from .env.example. vercel.json schedules the digest at 06:00 UTC (09:00 Helsinki) and a sync every three hours; on the Hobby plan only the daily digest will run, and members can use "Sync now" on their page. Adjust the cron hour for your group's timezone.
To post the reading by hand:
curl -H "Authorization: Bearer $CRON_SECRET" https://<your-domain>/api/cron/digestThe first person has no sponsor. Open /enter directly (no ?via=), connect a device, and share the mark from /me. Everyone after enters through someone.
pnpm typecheck # next typegen + tsc
pnpm test # vitest (rules, date handling)
pnpm lint- Apple Health / Garmin / Fitbit (an aggregator or bridge app; Apple Health has no cloud API)
- Enforcement of exile and the rite of return
- Token encryption at rest (service-role-only today; encrypt before the group is strangers)
- Webhooks from WHOOP and Oura (polling is enough at this size)
- An in-house chat, when pseudonymity needs to hold beyond people you already know
MIT