Skip to content

upneja/redthread

Repository files navigation

Red Knot Club

A personal matchmaking club for friends and friends-of-friends (New York first — SF waitlist — mid-late 20s, largely Asian-American). One long, research-backed questionnaire, read by a human matchmaker, assisted by Claude. No swiping, no public profiles, no app.

Production: https://redknotclub.com (Vercel project redthread; redthreadintros.vercel.app remains as a secondary alias)

Brand history: launched as "Red Thread" (2026-06-10), rebranded to Red Knot Club with the redknotclub.com purchase (2026-06-11). Same legend — the thread is fate's job, the knot is ours.

Architecture

  • Next.js 16 (App Router, Turbopack) + TypeScript strict + Tailwind v4 on Vercel.
  • Storage: encrypted document store on Vercel Blob — a private-access store (redthread-private), where every blob is additionally AES-256-GCM encrypted with an app-held key (DATA_ENCRYPTION_KEY) before upload. No public URL exists for any byte, and a leaked storage credential alone still yields only ciphertext.
    • applicants/<id>.json.enc — the application document
    • notes/<id>.json.enc — admin notes/log, separate so applicant edits can't clobber them
    • tokens/<sha256(token)>.json.enc — edit-token hash → applicant id (raw tokens never stored)
    • photos/<id>/<photoId>.enc — sharp-re-encoded (EXIF/GPS-stripped) encrypted images
    • Reads use get(pathname, { access: 'private', useCache: false }) for strongly consistent read-after-write (CDN-cached reads serve stale content after overwrites).
  • No accounts. Applicants get a 128-bit edit token in a private link (/edit/<token>); only its SHA-256 is stored. The single admin signs in with Google — OAuth authorization-code flow allowlisted to exactly ADMIN_EMAIL, id_token verified against Google JWKS — and gets a 7-day jose-signed httpOnly cookie.
  • Auth is checked in every admin route handler and page — no middleware/proxy as a security boundary.

The instrument (src/lib/questions.ts)

Two tiers, grounded in the research in docs/research-digest.md:

  • Express (~10 min, 6 sections + photos) — gets someone into the pool. Identity, intent/timeline/kids, the diaspora core (cultural background + whose preference it is, religion + practice + importance, parents' role, family expectations), dealbreakers, lifestyle, short psyche battery (attachment pick, TIPI-6, conflict style, life satisfaction), humor, and three free-texts.
  • Full (~25 min, 7 sections) — ECR-S 12-item attachment, values ranking, family & culture deep-dive, relationship history ("what would an ex say…"), daily texture, affection (skippable), and the bilateral preference block (OkCupid-style accept/importance on 8 keys) + date-me-doc pitch.

Editing the questionnaire = editing that one config file; rendering and validation are config-driven.

Matching (src/lib/matching.ts, src/lib/brief.ts)

Hard dealbreaker filters (mutual orientation, age windows, distance, kids, vice dealbreakers, do-not-match lists, bilateral non-negotiables) → weighted scoring (bilateral satisfaction geometric mean, values-rank overlap, religion/politics homogamy scaled by stated importance, lifestyle proximity, family alignment) → documented risk flags (anxious×avoidant, volatile×avoider, both-high-neuroticism, spender×saver). Scores are labeled "ideas, not verdicts" in the admin UI on purpose — the science says questionnaires can't predict chemistry, so the human + post-date feedback loop carry the real weight.

brief.ts generates fixed-schema Claude prompts (pair brief and ranked-pool brief) with contact info excluded by design.

The hub

/admin (Google sign-in, ayushupneja@gmail.com only) — one nav across six sections:

  • Desk — pool table → full profiles with decrypted photos, status workflow, notes + append-only log, the Evidence panel (transparent math, dossier prompts, propose-intro).
  • Intros — the concierge pipeline (proposed → asked → consented → introduced → outcome) and the north star: % of applicants with a consented intro.
  • Board — internal kanban (owner, status, blockers) for all work on the club.
  • Numbers — first-party anonymous analytics, funnel, intro funnel, pool liquidity by gender.
  • Pairs — every eligible pair ranked by the math engine.
  • Library — the company docs (docs/company/, embedded at build time).

Security posture

See docs/security-checklist.md. Highlights: private+encrypted storage; sharp re-encode strips EXIF/GPS and neutralizes malicious files; strict CSP / HSTS / no-referrer (edit tokens live in URLs); rate limiting (in-memory, per-instance) + honeypot on submission; zod validation against the question config; no analytics or third-party scripts anywhere; PII never logged.

Ops

  • npm run dev (uses .env.local, pulled via vercel env pull + the three app secrets)
  • npm run build && npm run lint before deploys; vercel deploy --prod --yes
  • Env vars (all three environments): BLOB_READ_WRITE_TOKEN, DATA_ENCRYPTION_KEY, SESSION_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, ADMIN_EMAIL, OAUTH_REDIRECT_BASE (OAuth client lives in GCP project aj-apartment-shortlist, shared with Creator OS; callback URIs registered for redknotclub.com, redthreadintros.vercel.app, and localhost:3000)
  • Losing DATA_ENCRYPTION_KEY loses all data. It's in Vercel env + .env.local.
  • Deployment protection is preview-only so production is public.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors