Serein
Rust로 만든 가볍고 성능 좋은 Discord 전용 네이티브 클라이언트
매주 수요일, 한 번의 매칭. Daisy Weekly는 실제 선호도 기반으로 몬트리올 학생들을 연결한다
Every Wednesday, one match. Daisy Weekly pairs Montreal students based on real preferences — not algorithms designed to waste your time.
소셜TypeScript · JavaScriptGitHub @Malik1234567891011온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
This document explains what Daisy is today, how the code is structured, how weekly matching works, and how to operate it safely.
Daisy is a Montreal student matching app:
• users onboard with profile + preferences + phone verification,
• receive one weekly match,
• choose interested/decline,
• unlock contact info only on mutual interest.
The project includes:
• the public web app (Next.js),
• server APIs (App Router route handlers),
• weekly matching generation scripts,
• weekly seeding + SMS scripts,
• admin and local operational scripts.README 전문 보기 ↗This document explains what Daisy is today, how the code is structured, how weekly matching works, and how to operate it safely.
Daisy is a Montreal student matching app:
The project includes:
Key files:
package.jsonprisma/schema.prismasrc/lib/auth.tssrc/lib/db.tssrc/app/**/page.tsx.src/app/api/**/route.ts.src/proxy.ts.scripts/.../matches.md (outside daisy/).Primary file: src/app/onboarding/page.tsx
Flow:
POST /api/signup),POST /api/upload),POST /api/otp/send, POST /api/otp/verify).Important:
src/app/login/page.tsxsrc/app/api/auth/[...nextauth]/route.tssrc/lib/auth.tssrc/proxy.ts (/dashboard, /profile, /preferences, /verify-phone, and /admin via the admin cookie).src/app/dashboard/page.tsxGET /api/matchPOST /api/match/decisionBehavior:
PENDING, MUTUAL),src/app/forgot-password/page.tsxsrc/app/reset-password/page.tsx -> /forgot-passwordPOST /api/auth/forgot-passwordPOST /api/auth/reset-passwordThis flow uses Twilio Verify on the user's verified phone, not email-link reset.
DELETE /api/user/deleteSchema: prisma/schema.prisma
Core models:
User
Match
userAId + userBId, decisions per side, overall status, dropDatesuggestedSpotIdMeetingSpot
BroadcastDedupe
Enums:
MatchDecision: PENDING, INTERESTED, DECLINEDMatchStatus: PENDING, MUTUAL, EXPIRED, DECLINEDScript: scripts/generate-matches-md.js
What it does:
phoneVerified && onboardingComplete),../matches.md,Drop slot of WED,Script: scripts/seed-week-matches-and-broadcast.js
Dry run:
node scripts/seed-week-matches-and-broadcast.js
Live run:
DAISY_BROADCAST_CONFIRM=yes node scripts/seed-week-matches-and-broadcast.js --seed --sms
Safety behavior (current implementation):
PENDING/MUTUAL -> EXPIRED),dropDate per pair from Drop slot (WED, FRI, SUN),dropDate <= now) and only to users with phoneVerified, phoneNumber, smsConsent.This script parses pair details from ../matches.md and then creates DB rows.
Implemented in scripts/generate-matches-md.js:
Dawson, Kamil, Vincent, Malik),confessions, @office.),vercel.json.src/app/api/cron/wednesday-broadcast/route.tssrc/lib/wednesdayBroadcastSms.tsBroadcastDedupe table by period key. The seed script writes the same key after its --sms run, so the cron skips that day instead of texting matched users twice.dropDate in the last 48h (not every verified member). Sends only Wednesday 5:00–7:59 PM Toronto; a second cron at 22:30 UTC retries if the first slipped.src/lib/sms.ts and respect smsConsent./admin (src/app/admin/page.tsx) — signups, verification
funnel, breakdowns, referrers, suspect emails, and a profile viewer with
delete. Sign in at /admin/login with ADMIN_API_KEY; the session is a
cookie signed with AUTH_SECRET (src/lib/admin-auth.ts).src/app/api/admin/match/route.tssrc/app/api/admin/notify/route.tsDELETE /api/admin/users/[id]x-admin-key header
matching ADMIN_API_KEY.Useful scripts:
scripts/check-users.js - quick user check scriptscripts/reset-match-between.js - reset/create pair between two usersscripts/broadcast-daisy-wednesday.js - broadcast utilityscripts/send-match-sms-one.js - one-off SMSscripts/wipe-db.js - destructive reset script (matches/users/blob cleanup)Install:
npm install
Run app:
npm run dev
Build:
npm run build
Start production build:
npm run start
Lint:
npm run lint
Database:
DATABASE_URLDIRECT_URLAuth/session:
AUTH_SECRETAUTH_URL / NEXTAUTH_URL / VERCEL_URL (base URL helpers)Twilio:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_PHONE_NUMBERTWILIO_VERIFY_SERVICE_SIDStripe billing (pay-per-reroll, $1.99 CAD one-time):
STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_REROLL_PRICE_ID (optional — a real Price for cleaner reporting; if
unset, checkout builds the line item inline from REROLL_PRICE_CENTS)Broadcast/cron:
CRON_SECRETBROADCAST_SITE_URLNEXT_PUBLIC_URLBROADCAST_SUPPORT_EMAILBROADCAST_BODYBROADCAST_REQUIRE_RECENT_DROPDAISY_BROADCAST_CONFIRM (script safety)Admin:
ADMIN_API_KEYOther:
NEXT_PUBLIC_GA_IDBLOB_READ_WRITE_TOKEN (for blob cleanup script)matches.md is the review layer before DB writes.POST /api/reroll/checkout opens a $1.99 CAD Stripe Checkout in payment mode; POST /api/reroll spends the resulting credit, closes the current match as REROLLED (if it was still PENDING), and opens a new one via src/lib/matching.ts. A match that closed this week (declined by either side, or rerolled away) can also be rerolled, so "Not for me" doesn't lose the upsell. The person rerolled away from gets a free re-match when the pool allows, and everyone who receives a new match is texted.smsConsent is set from the checkbox on the phone step (POST /api/otp/verify), not assumed at signup, and can be turned off on the profile page.POST /api/stripe/webhook and is required to grant reroll credits. It must be subscribed to checkout.session.completed and checkout.session.async_payment_succeeded.This section tracks the core business metrics and gives a quick way to refresh them.
Run this from daisy/:
node -e "const fs=require('fs'); const path=require('path'); ['.env','.env.local'].forEach(f=>{const p=path.resolve(__dirname,f); if(!fs.existsSync(p)) return; fs.readFileSync(p,'utf8').split('\n').forEach(line=>{const m=line.match(/^\s*([A-Z_][A-Z0-9_]*)=[\"']?(.+?)[\"']?\s*$/); if(m && !process.env[m[1]]) process.env[m[1]]=m[2];});}); const {PrismaClient}=require('@prisma/client'); const prisma=new PrismaClient(); (async()=>{ const totalUsers=await prisma.user.count(); const verified=await prisma.user.count({where:{phoneVerified:true}}); const onboarded=await prisma.user.count({where:{onboardingComplete:true}}); const latest=await prisma.match.findFirst({orderBy:{dropDate:'desc'},select:{dropDate:true}}); let week={matches:0,replied:0,totalPeople:0,interested:0,declined:0,pending:0,mutual:0,waiting:0,replyRate:0,interestRate:0}; if(latest){ const rows=await prisma.match.findMany({where:{dropDate:latest.dropDate},select:{status:true,userADecision:true,userBDecision:true}}); let i=0,d=0,p=0; for(const r of rows){ for(const x of [r.userADecision,r.userBDecision]){ if(x==='INTERESTED') i++; else if(x==='DECLINED') d++; else p++; }} const replied=i+d; const totalPeople=rows.length*2; const mutual=rows.filter(r=>r.status==='MUTUAL').length; const waiting=rows.filter(r=>(r.userADecision==='INTERESTED'&&r.userBDecision==='PENDING')||(r.userBDecision==='INTERESTED'&&r.userADecision==='PENDING')).length; week={matches:rows.length,replied,totalPeople,interested:i,declined:d,pending:p,mutual,waiting,replyRate:totalPeople?Math.round(replied/totalPeople*100):0,interestRate:replied?Math.round(i/replied*100):0,dropDate:latest.dropDate.toISOString()}; } console.log(JSON.stringify({totalUsers,verified,onboarded,week},null,2)); await prisma.$disconnect(); })();"
If wanted, we can add: