Every Wednesday, one match. Daisy Weekly pairs Montreal students based on real preferences — not algorithms designed to waste your time.
Translation pending
SocialTypeScript · JavaScriptGitHub @Malik1234567891011OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
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.Read the full 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: