尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
"Uber voor barbers" — een marketplace voor mobiele zzp-barbers. Klanten boeken een barber op locatie; de betaling gaat via Stripe in escrow en wordt na afronding vrijgegeven aan de barber, tenzij binnen 24 uur een geschil wordt geopend. Dit document is de technische onboarding: hoe je het project lokaal draaiend krijgt, hoe alles is opgezet, en hoe je naar productie deployt. Voor de volledige bouwgeschiedenis, architectuurkeuzes per fase en bekende openstaande punten, zie PROJECT.md en CLAUDE.md.檢視完整 README ↗
"Uber voor barbers" — een marketplace voor mobiele zzp-barbers. Klanten boeken een barber op locatie; de betaling gaat via Stripe in escrow en wordt na afronding vrijgegeven aan de barber, tenzij binnen 24 uur een geschil wordt geopend.
Dit document is de technische onboarding: hoe je het project lokaal
draaiend krijgt, hoe alles is opgezet, en hoe je naar productie
deployt. Voor de volledige bouwgeschiedenis, architectuurkeuzes per fase
en bekende openstaande punten, zie PROJECT.md en
CLAUDE.md.
| Laag | Keuze |
|---|---|
| Framework | Next.js 15 (App Router), React 19, TypeScript |
| Styling | Tailwind CSS 3, custom design tokens (tailwind.config.ts) |
| Database/Auth/Storage | Supabase (Postgres, Row Level Security, Storage, Auth) |
| Betalingen | Stripe (Payment Intents, Connect Express, escrow via Transfers) |
| Resend | |
| Push-notificaties | Web Push (VAPID), eigen service worker (public/sw.js) |
| Rate limiting | Upstash Redis (@upstash/ratelimit) |
| Error monitoring | Sentry (@sentry/nextjs) |
| Hosting | Vercel |
Geen ORM — alle database-toegang gaat via de Supabase JS-client
(@supabase/supabase-js/@supabase/ssr), met RLS-policies als
autorisatielaag in plaats van applicatiecode.
package.json/package-lock.json
verwacht — geen exotische versie nodig, elke recente LTS werkt).npm install -g supabase, of laat npx supabase
het per-project installeren — hoeft niet globaal).stripe listen) — stripe.com/docs/stripe-cli.git clone <repo-url> groomy
cd groomy
npm install
cp .env.local.example .env.local
# vul .env.local in — zie "Supabase instellen" en "Stripe instellen" hieronder
npm run dev
De app draait dan op http://localhost:3000. Er is geen seed-data — je
hebt minimaal één klant-account en één (goedgekeurd) barber-account
nodig om de boekingsflow te doorlopen. Zie "Eerste gebruikers aanmaken"
hieronder.
npx supabase login
npx supabase link --project-ref <jouw-project-ref>
Het project-ref vind je in de Supabase-dashboard-URL
(supabase.com/dashboard/project/<ref>) of onder Project Settings →
General.
Alle schema, RLS-policies, triggers en functies staan als losse,
oplopend genummerde bestanden in supabase/migrations/. Nooit
handmatig in de SQL Editor knoeien buiten migraties om — dat raakt
onherleidbaar uit sync met wat hier in git staat.
npx supabase db push
Dit voert alle 16 migraties in volgorde uit (genummerd 0001–0017,
0008 is destijds bewust nooit gebruikt/aangemaakt). Ze zijn
idempotent-veilig te herdraaien vanaf een leeg project, maar niet ontworpen om
individueel over te slaan — voer ze altijd in volgorde uit.
De automatische vrijgave van escrow-betalingen (24 uur na afronding,
zie 0011_escrow_release_cron.sql) draait via Postgres' eigen
pg_cron/pg_net-extensies, niet via een externe cron-service.
Zet ze aan in het dashboard onder Database → Extensions vóórdat je
migratie 0011 pusht (kan niet vanuit een migratie zelf).
Na de eerste deploy moet je ook app_config bijwerken met je echte
deploy-URL en CRON_SECRET (staat in de migratie als placeholder):
update public.app_config set value = 'https://jouw-deploy-url.vercel.app' where key = 'api_base_url';
update public.app_config set value = 'jouw-cron-secret' where key = 'cron_secret';
Zolang api_base_url nog de placeholder-waarde is, slaat de cron
zichzelf bewust over (met een notice in de Postgres-logs) in plaats
van te falen tegen een niet-bestaande URL — lokaal ontwikkelen wordt
hier dus niet door geblokkeerd.
Worden automatisch aangemaakt door migratie 0004_barber_verification.sql
(barber-media, publiek — portfoliofoto's; barber-documents, privé —
KvK/verzekering/diploma). Geen handmatige stap nodig.
Onder Authentication → URL Configuration:
http://localhost:3000
lokaal, je Vercel-URL in productie).<jouw-url>/auth/confirm toe — dit is de
route die e-mailbevestigingslinks afhandelt (src/app/auth/confirm/route.ts).Supabase's gratis mailquota is laag en rate-limit't snel tijdens
ontwikkelen/testen. Voor testaccounts die niet via een echte inbox
hoeven te gaan, gebruik de Admin API in plaats van signUp():
curl -X POST '<NEXT_PUBLIC_SUPABASE_URL>/auth/v1/admin/users' \
-H "apikey: <SUPABASE_SERVICE_ROLE_KEY>" \
-H "Authorization: Bearer <SUPABASE_SERVICE_ROLE_KEY>" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "Test123!",
"email_confirm": true,
"user_metadata": { "role": "customer", "full_name": "Test Klant", "phone": "0612345678" }
}'
Voor een barber-account: "role": "barber" — die belandt na inloggen
automatisch in de aanmeld-flow (/barber/aanmelden), en moet daarna via
het adminpanel op approved gezet worden voordat die zichtbaar is voor
klanten (zie hieronder).
Er is bewust geen registratieformulier voor admins (zie "Architectuur").
Maak het eerste account op dezelfde manier als hierboven, met
"role": "admin" in user_metadata — de handle_new_user()-trigger
zet die dan automatisch in admin_users in plaats van profiles. Log
daarna in op /admin/login.
Dashboard → Developers → API keys (test mode). STRIPE_SECRET_KEY en
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in .env.local.
De app luistert op POST /api/stripe/webhook voor
payment_intent.succeeded en account.updated — dit is de enige
plek waar een payments-rij mag ontstaan (nooit op basis van een
client-side "succes"-signaal). Lokaal forward je events met de Stripe
CLI:
stripe listen --forward-to localhost:3000/api/stripe/webhook
Dit print een whsec_...-signing secret — zet die in
STRIPE_WEBHOOK_SECRET.
Na de eerste Vercel-deploy: Dashboard → Developers → Webhooks → nieuw
endpoint tegen https://<jouw-domein>/api/stripe/webhook, events
payment_intent.succeeded en account.updated. Het bijbehorende
signing secret is anders dan je lokale — apart invullen in Vercel's
env vars, niet hergebruiken.
Geen aparte configuratie nodig — barbers doorlopen Stripe's gehoste
Express-onboarding vanuit /barber/uitbetalingen
(src/app/api/stripe/connect-onboarding/route.ts maakt het account
aan). Voor test mode kun je Stripe's testgegevens gebruiken om de
onboarding-flow af te ronden.
Test-mode-sleutels → live-mode-sleutels, plus een nieuw
webhook-endpoint tegen je productie-URL (levert een nieuw secret op).
Zie de checklist in PROJECT.md → "Pre-launch audit — architectuur"
voor de volledige livegang-stappen.
Volledige, actuele lijst staat in .env.local.example
met toelichting per var. Samengevat:
| Var | Verplicht lokaal? | Bron |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Ja | Supabase → Project Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Ja | Supabase → Project Settings → API |
SUPABASE_SERVICE_ROLE_KEY | Ja | Supabase → Project Settings → API (geheim, nooit client-side) |
STRIPE_SECRET_KEY | Ja | Stripe → Developers → API keys |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Ja | Stripe → Developers → API keys |
STRIPE_WEBHOOK_SECRET | Ja | stripe listen (lokaal) of het webhook-endpoint (productie) |
CRON_SECRET | Ja | Zelf verzinnen — moet gelijk zijn aan app_config.cron_secret |
RESEND_API_KEY | Ja | Resend → API Keys |
RESEND_FROM_EMAIL | Ja | Een adres in je Resend-account (sandbox: alleen je eigen accountadres) |
NEXT_PUBLIC_VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY | Ja | npx web-push generate-vapid-keys (eenmalig) |
VAPID_SUBJECT | Ja | mailto:[email protected] |
UPSTASH_REDIS_REST_URL / _TOKEN | Nee | Upstash-dashboard — zonder deze vars is er lokaal simpelweg geen rate limit |
NEXT_PUBLIC_SENTRY_DSN | Nee | Sentry-project → Client Keys — zonder deze var init't Sentry niet (geen foutmelding) |
NEXT_PUBLIC_SITE_URL | Nee | Absolute basis-URL (OG-tags/sitemap) — valt lokaal terug op localhost:3000 |
app_config.api_base_url in Supabase bij naar
de echte deploy-URL (zie stap 3 onder "Supabase instellen") — pas
dan gaat de escrow-release-cron echt draaien.NEXT_PUBLIC_SITE_URL bijwerken.Er is bewust geen vercel.json met een cron-config — de
escrow-release draait al via Supabase's pg_cron, een Vercel Cron zou
dat alleen maar dupliceren.
Voor de volledige livegang-checklist (Resend-domein verifiëren, Stripe
naar live mode, testdata opschonen, smoke test) zie PROJECT.md →
"Pre-launch audit — architectuur" → "Checklist voor live gaan".
src/
app/
klant/ klant-app (mobiel, in PhoneShell-frame)
barber/ barber-app (mobiel, in PhoneShell-frame)
admin/ adminpanel (desktop, geen PhoneShell)
api/ Route Handlers — Stripe, wallet, admin-mutaties, cron, geocode
auth/ e-mailbevestiging/foutafhandeling voor Supabase Auth
{error,not-found,global-error}.tsx app-brede foutschermen
components/
ui/ designsysteem-primitieven (Button, Input, Dialog, ...)
shared/ samengestelde componenten (PhoneShell, Row, EscrowDot, ...)
admin/ adminpanel-specifieke tabellen/formulieren
wallet/ wallet-topup-flow (klant + barber gedeeld)
lib/
supabase/ client-/server-/service-role-clients, alle DB-queries (queries.ts), admin-auth
stripe.ts / stripe-client.ts server- resp. client-side Stripe SDK
pricing.ts enige bron van waarheid voor prijsberekening (15% platform fee)
wallet.ts wallet/loyaliteit-constantes
rate-limit.ts Upstash-gebaseerde rate limiter
resend.ts / push.ts e-mail resp. web-push-verzending
middleware.ts alle route-/rolbeveiliging (zie hieronder)
supabase/
migrations/ genummerde SQL-migraties — schema, RLS, triggers, functies
Drie totaal gescheiden identiteiten, geen gedeelde tabel:
profiles-rij per gebruiker, met
role: 'customer' | 'barber' uit auth.users.user_metadata.role (JWT-
claim, gezet bij registratie). barber_profiles/customer_profiles
zijn 1:1-extensietabellen.admin_users-tabel, geen profiles-rij.
Geen registratieformulier — het eerste account wordt via de Supabase
Admin API aangemaakt (zie hierboven). Middleware herkent een
adminsessie aan user_metadata.role === 'admin', exact hetzelfde
mechanisme als klant/barber.middleware.ts is de enige plek die rol-/sessiebeveiliging voor
/klant/*, /barber/* en /admin/* afdwingt — geen dubbele guards in
individuele pagina's. Een klant/barber die op /admin uitkomt wordt
stil naar de eigen home gestuurd (geen foutmelding die verraadt dat er
een adminpanel bestaat).
Zo goed als elke tabel heeft row level security aan, met policies
die scopen op auth.uid(). Tabellen met alleen server-side schrijfpaden
(payments, admin_users, admin_action_log, discount_codes, ...)
hebben revoke all from anon, authenticated — die zijn alleen
bereikbaar via createServiceClient() (de service-role-client, altijd
server-side, nooit in een "use client"-bestand). Zie
src/lib/supabase/{client,server,service}.ts voor de drie
clientvarianten en wanneer welke te gebruiken.
Belangrijke valkuil (zie ook CLAUDE.md regel 7): een RLS-policy
alleen is niet genoeg — kolom-niveau grants moeten er vaak bovenop.
Voorbeeld: bookings had tot de pre-launch audit een ongescoopte
INSERT-grant, waardoor een klant élk veld in een nieuwe boeking kon
zetten (inclusief status: 'completed' zonder ooit te betalen). Zie
0017_prelaunch_audit_fixes.sql voor de fix (een before insert-
trigger die prijs/status server-side afdwingt, plus een kolom-scoped
grant).
/api/stripe/create-payment-intent) — bedrag komt uit
computePriceBreakdown() (src/lib/pricing.ts), nooit uit
client-input.payment_intent.succeeded (webhook, geverifieerd met
STRIPE_WEBHOOK_SECRET) ontstaat de payments-rij met
escrow_state: 'held'.bookings.status → 'completed') draait elke 15
minuten 0011's pg_cron-job, die /api/cron/release-escrow
aanroept. Die geeft een betaling pas vrij (transfers.create naar
de barber's Stripe Connect-account) als: 24 uur verstreken sinds
afronding, geen open geschil, en de barber's Connect-account
stripe_payouts_enabled is (server-side geverifieerd via de
account.updated-webhook, nooit client-schrijfbaar sinds 0017)./klant/geschil) blokkeert de
vrijgave; een admin lost het op via /admin/geschillen
(terugbetalen of alsnog vrijgeven).find_nearest_eligible_barber() (SQL-functie, 0007_matching.sql)
zoekt de dichtstbijzijnde online, approved-barber binnen diens eigen
werkstraal die de gevraagde dienst aanbiedt. Een broadcast-aanvraag
(bookings.barber_id = null) is voor elke eligible barber zichtbaar;
claimBooking() claimt 'm atomisch
(update ... where barber_id is null and status = 'requested') — bij
een race wint er maar één barber, de rest krijgt een nette
"al vergeven"-melding.
Desktop-georiënteerd (geen PhoneShell), onder /admin/*. Elke
schrijvende actie gaat via een /api/admin/*-route die eerst
requireAdmin() checkt (src/lib/supabase/admin.ts) en daarna
logAdminAction() aanroept — een compleet, chronologisch logboek van
elke adminactie staat op /admin/logboek.
error.tsx/global-error.tsx/not-found.tsx vangen respectievelijk
runtime-fouten binnen de layout, fouten in de layout zelf, en 404's —
alle drie sturen naar Sentry als NEXT_PUBLIC_SENTRY_DSN gezet is.
npm run dev # lokale dev-server (localhost:3000)
npm run build # productie-build + type-check + lint
npm run lint # alleen lint
npx supabase db push # migraties naar het gelinkte Supabase-project pushen
npx supabase migration new <naam> # nieuwe, oplopend genummerde migratie aanmaken
stripe listen --forward-to localhost:3000/api/stripe/webhook # lokaal webhooks testen
Draai npm run build altijd vóór je iets als "af" beschouwt — dat is
zowel de type-check als de lint-check in één.