const OPENAI_KEY = "sk-xxxxxxxxxxxxxxxx"
const SUPABASE_KEY = "eyJhbGciOi..."
- Maak een bestand
.envaan in je project root - Zet daarin:
OPENAI_API_KEY=sk-jouwkey - Voeg
.enven.env.localtoe aan.gitignore - Gebruik in je code:
process.env.OPENAI_API_KEY - Voeg de variabelen in bij Vercel, via Project, Settings, Environment Variables
- Geen API-key hardcoded in de codeZoek in je project op
sk-,eyJ,pk_live. Als je iets vindt, verplaats het direct naar .env. - .env staat in .gitignoreOpen je
.gitignoreen controleer of.enverin staat. Anders: voeg het toe en rungit rm --cached .envals het al getrackt werd. - .env.local staat in .gitignoreNext.js gebruikt .env.local voor lokale overschrijvingen. Zorg dat beide varianten geblokkeerd zijn.
- Geen API-keys in GitHub commits (check historie)Ga naar je repo, GitHub, Security, Secret Scanning. Of installeer gitleaks lokaal voor een grondige scan.
- Alle secrets staan in Vercel Environment VariablesVercel, jouw project, Settings, Environment Variables. Kies welke variabelen voor Production, Preview en Development gelden.
- Verschillende keys voor Development en ProductionMaak aparte Stripe-test vs -live keys, aparte Supabase projecten of aparte OpenAI keys per omgeving. Zo ga jij bij testen nooit per ongeluk productiedata aan.
- Ga naar je GitHub repository
- Klik op Settings
- Scroll naar Security, dan Code security and analysis
- Zet Secret scanning aan
- Optioneel: zet ook Push protection aan, dat blokkeert een push als er een key in zit
- Repository is Private (tenzij bewust open source)GitHub, jouw repo, Settings, Change repository visibility. Bij twijfel: private.
- Geen database wachtwoorden in commitsDoorzoek je commits:
git log -p | grep -i "password". Als er iets gevonden wordt: verander het wachtwoord en verwijder het uit de git-historie. - Geen Supabase Service Role Key gepushtDe Service Role Key geeft volledige toegang tot je database, ook zonder RLS. Deze mag nooit in je code. Zoek op
service_rolein je repo. - Geen Stripe Secret Key gepushtDe Stripe Secret Key begint met
sk_live_ofsk_test_. Zoek daar op in je codebase en commit-historie. - Geen MailerLite of andere API Keys gepushtGeldt voor alle third-party services: MailerLite, Resend, Postmark, etc. Gebruik altijd environment variables.
- Branch protection aan voor mainSettings, Branches, Add branch protection rule. Vink aan: "Require pull request reviews before merging." Zo push je nooit per ongeluk rechtstreeks naar productie.
Als een onbekende persoon via de browser jouw klantdata kan uitlezen. Test dit: open een incognitovenster en probeer data op te halen via de Supabase REST API. Lukt het? Dan staat RLS uit.
- Ga naar Supabase Dashboard, jouw project, Table Editor
- Klik op de tabel, Edit Table
- Zet Enable Row Level Security (RLS) aan
- Ga daarna naar Authentication, Policies
- Voeg een policy toe: bijv.
auth.uid() = user_idzodat gebruikers alleen hun eigen rijen zien - Herhaal voor elke tabel met gevoelige data
- Row Level Security (RLS) staat aan voor alle tabellenSupabase, Table Editor, klik op elke tabel, controleer of RLS enabled is. Zonder RLS is je database publiek.
- Tabellen zijn niet publiek leesbaar zonder beleidRLS aan betekent nog niet dat er een policy is. Ga naar Authentication, Policies en zorg dat elke tabel minimaal één SELECT-policy heeft.
- Tabellen zijn niet publiek schrijfbaarVoeg ook INSERT/UPDATE/DELETE policies toe. Zonder policy en met RLS aan: niets kan. Maar bij een "public" policy: iedereen kan alles.
- Ingelogde gebruikers zien alleen hun eigen dataStandaard policy:
USING (auth.uid() = user_id). Dit zorgt dat user A nooit de data van user B ziet. - Service Role Key wordt alleen server-side gebruiktDe Service Role Key mag NOOIT in frontend-code (React, Next.js client components). Alleen in API routes, server actions of Edge Functions. Nooit in de browser.
// NOOIT: prijs komt van de gebruiker
const price = req.body.price
await stripe.checkout.sessions.create({ line_items: [{ price }] })
// GOED: prijs staat hardcoded in je server-code
const CURSUS_PRIJS = 'price_xxxYourStripeProductIDxxx'
await stripe.checkout.sessions.create({ line_items: [{ price: CURSUS_PRIJS, quantity: 1 }] })
- Stripe Dashboard, Developers, Webhooks, Add endpoint
- Vul jouw Vercel URL in:
https://jouwnaam.vercel.app/api/webhook - Selecteer events:
checkout.session.completed,invoice.paid - Kopieer de Webhook Signing Secret naar je .env als
STRIPE_WEBHOOK_SECRET - Verifieer elke webhook met
stripe.webhooks.constructEvent()
- Stripe Secret Key staat alleen server-sideDe key die begint met
sk_live_mag nooit in frontend-code. Gebruik de Publishable Key (pk_live_) voor de browser en de Secret Key alleen in API routes. - Webhooks zijn geconfigureerd en geverifieerdZonder webhook-verificatie kan iemand een nep-betaling sturen naar jouw endpoint en toch toegang krijgen. Gebruik altijd
stripe.webhooks.constructEvent(). - Prijzen komen nooit van de frontendGebruik altijd Stripe Price IDs die je in je Stripe Dashboard aanmaakt. Nooit een bedrag dat de gebruiker invoert of dat door de frontend wordt meegestuurd.
- Checkout sessie wordt server-side aangemaaktDe browser roept een API route aan, die API route maakt de Stripe sessie, de browser wordt doorgestuurd naar de Stripe betaalpagina. Nooit direct vanuit de browser.
Open een incognitovenster. Probeer handmatig naar /admin, /dashboard, /account te navigeren. Word je doorgestuurd naar de loginpagina? Dan is het goed. Kom je er gewoon in? Dan is er een probleem.
- Login verplicht waar nodigVoeg middleware toe (bijv. in Next.js:
middleware.ts) die routes beschermt. Bij Supabase Auth: check altijdgetUser()server-side. - Admin pagina's zijn afgeschermd op rolHet is niet genoeg om een link te verbergen. De route zelf moet controleren of de ingelogde gebruiker admin-rechten heeft. Voeg een
roleofis_adminkolom toe in Supabase. - Wachtwoord reset werkt correctTest dit zelf: vraag een wachtwoord reset aan met een bestaand e-mailadres. Komt de mail aan? Werkt de reset link? Wordt er ingelogd na reset? Allemaal testen.
- Sessies verlopen automatischSupabase Auth doet dit standaard (JWT expiry). Check dat je de sessie niet oneindig verlengt zonder re-authenticatie. Supabase vernieuwt tokens automatisch maar je kunt de duur instellen via Auth Settings.
- Admin interface is niet zichtbaar voor gewone bezoekersZowel qua navigatie (niet tonen) als qua routing (redirect als je probeert in te gaan). Dubbele bescherming: UI en server-side check.
// NOOIT zo: directe string interpolatie in SQL
const query = `SELECT * FROM users WHERE email='${email}'`
// GOED: gebruik Supabase client (parameterized queries)
const { data } = await supabase.from('users').select().eq('email', email)
- Spam beveiliging aanwezig (honeypot of Turnstile)Voeg een honeypot-veld toe (verborgen input die mensen niet invullen maar bots wel) of gebruik Cloudflare Turnstile (gratis, privacy-vriendelijk alternatief voor reCAPTCHA).
- Rate limiting ingesteld op formulier-endpointsVercel heeft ingebouwde rate limiting via Edge Middleware. Vercel KV of Upstash Redis zijn populaire keuzes. Basisregel: max 5 submissions per minuut per IP.
- Input validatie aan beide kanten (client en server)Frontend-validatie is voor UX. Server-side validatie is voor veiligheid. Gebruik Zod voor schema-validatie:
z.string().email().max(255). Vertrouw nooit de input van de browser. - Geen raw SQL met user input (gebruik ORM of Supabase client)De Supabase JavaScript client gebruikt automatisch parameterized queries. Gebruik je Supabase goed? Dan ben je al beschermd. Gebruik je raw
supabase.rpc()met user input? Dan moet je zelf sanitizen.
- Alleen toegestane bestandstypes geaccepteerdControleer het MIME-type server-side, niet alleen de extensie. Extensies zijn eenvoudig te vervalsen. Voorbeeld:
if (!['image/jpeg','image/png','image/webp'].includes(file.type)). - Maximale bestandsgrootte ingesteldStel een limiet in: bijv. max 5MB voor afbeeldingen. In Next.js:
export const config = { api: { bodyParser: { sizeLimit: '5mb' } } }. Bij Supabase Storage: stel het in via bucket-instellingen. - Geen uitvoerbare bestanden toegestaan (.php, .exe, .sh)Blokkeer expliciet:
.php,.exe,.sh,.py,.jsals uploads. Een whitelist is veiliger dan een blacklist: sta alleen toe wat je nodig hebt. - Geüploade bestanden opgeslagen in Supabase Storage (niet public folder)Gebruik Supabase Storage buckets, niet de public-map van je Next.js project. Stel in of een bucket publiek of privé is. Voor profielfoto's: publiek. Voor documenten: privé met signed URLs.
Kan iemand jouw /api/chat endpoint onbeperkt aanroepen zonder in te loggen? Dat is een open rekening. Zet altijd minimaal authenticatie en rate limiting op AI-endpoints.
- OpenAI/Anthropic API key blijft server-sideAlle AI-aanroepen gaan via een API route (
/api/chat,/api/generate). De key staat in je environment variables en nooit in client-code. Gebruikprocess.env.OPENAI_API_KEY. - Geen klantgegevens naar OpenAI gestuurd zonder toestemmingNamen, e-mailadressen, adressen zijn persoonsgegevens. Vermeld in je privacyverklaring dat je AI gebruikt. Vraag toestemming voor verwerking of anonimiseer de data voor je het naar de API stuurt.
- Prompts worden niet gelogd met persoonsgegevensAls je logging gebruikt voor debugging: zorg dat namen, e-mails en andere persoonsgegevens niet in de logs terechtkomen. Gebruik masking of log alleen de anonieme structuur.
- Rate limits ingesteld op AI-endpointsZonder rate limit kan één gebruiker (of bot) je honderden euro's kosten in één uur. Gebruik Upstash Rate Limiter of Vercel Edge Middleware. Stel in: max 10 requests per minuut per gebruiker.
- Kostenlimiet ingesteld bij OpenAI/AnthropicOpenAI: Platform, Billing, Usage limits, stel een maandelijks hard limit in. Anthropic: idem via de console. Stel ook een soft limit in voor een waarschuwing. Dit is je vangnet als rate limiting faalt.
- Environment variables correct ingesteld voor ProductionVercel, Project, Settings, Environment Variables. Controleer dat alle .env variabelen hier ook staan en dat ze op "Production" gezet zijn (niet alleen Preview).
- Debug mode / verbose logging staat uit in productieZorg dat
NODE_ENV=productionactief is. Vercel doet dit automatisch voor productie-deploys. Controleer dat je geenconsole.log(userData)of error-details naar de browser stuurt. - Productie domein is gekoppeldVercel, Project, Settings, Domains. Voeg je eigen domein toe en verwijder de
vercel.appURL als je wilt dat de canonical URL jouw domein is (goed voor SEO en beveiliging). - SSL actief (HTTPS) op alle domeinenVercel regelt SSL automatisch via Let's Encrypt. Controleer of je domein groen is in Vercel, Domains. Is er een fout? Controleer je DNS-instellingen (CNAME of A-record naar Vercel).
- Geen gevoelige data zichtbaar in Vercel Function LogsVercel, Project, Functions, bekijk de logs. Zie je API-keys, wachtwoorden of persoonsgegevens voorbijkomen? Dan log je te veel. Verwijder gevoelige console.logs.
npm audit scant je project op bekende problemen en is één commando.npm audit
npm audit fix
npm updateRun dit in de map van je project. Bij "high" of "critical" meldingen: fix ze voor je live gaat. Bij "moderate": afwegen.
- npm audit uitgevoerd, geen critical of high kwetsbaarhedenRun
npm auditin je projectmap. Zie je kritieke meldingen? Run eerstnpm audit fix. Zijn er daarna nog issues? Zoek naar alternatieven voor die packages. - Kritieke dependencies zijn up-to-dateControleer
package.json: zijn de versies van Next.js, Supabase client, Stripe en React recent? Gebruiknpx npm-check-updatesvoor een overzicht. - Geen packages die al jaren niet onderhouden zijnKijk op npmjs.com naar de "last published" datum van packages. Een package met de laatste update uit 2019 en geen maintenance-bericht is een risico. Zoek een alternatief.
- Privacyverklaring aanwezig en toegankelijkZet een link naar je privacyverklaring in de footer en bij elk formulier. Vermeld welke data je verzamelt, waarvoor en met welke tools (Supabase, Stripe, MailerLite, OpenAI). Gratis generator: iubenda.com of cookieyes.com.
- Cookie melding aanwezig (als je analytics of tracking gebruikt)Gebruik je Google Analytics, Meta Pixel, of andere tracking? Dan is een cookiebanner verplicht. Gebruik je alleen first-party analytics (Plausible, Vercel Analytics)? Dan is het simpeler. Geen tracking betekent geen banner nodig.
- Verwerkersovereenkomst aanwezig voor MailerLiteMailerLite: Log in, Settings, Data Privacy, Data Processing Agreement. Download en bewaar. Dit is verplicht als je EU-abonnees hebt.
- Verwerkersovereenkomst aanwezig voor StripeStripe heeft een standaard Data Processing Agreement (DPA). Stripe Dashboard, Settings, Privacy & Legal, Data Processing Agreement. Accepteren en opslaan.
- Contactformulier vermeldt hoe data verwerkt wordtVoeg bij elk formulier een kort zinnetje toe: "Je gegevens worden gebruikt om op je bericht te reageren en worden niet gedeeld met derden." Met een link naar de privacyverklaring.
- Supabase Dashboard, Project, Settings, Backups
- Op de gratis tier: dagelijkse backups voor 7 dagen
- Op Pro tier: Point-in-Time Recovery (PITR) beschikbaar
- Test je backup: herstel hem eens op een test-project om te controleren dat het werkt
- Supabase database backup is actiefSupabase, Settings, Backups. Gratis plan heeft 7 dagen daily backups. Controleer dat ze actief zijn en noteer waar je ze kunt terugzetten.
- Code staat in GitHub (nooit alleen lokaal)Push elke werkdag je wijzigingen naar GitHub. Stel desnoods een reminder in. Als je laptop crasht, is GitHub je backup van je hele project.
- Stripe transactiedata is exporteerbaarStripe Dashboard, Reports, Exports. Je kunt betalingen, klanten en facturen exporteren als CSV. Doe dit maandelijks als administratieback-up.
- MailerLite abonneelijst is exporteerbaarMailerLite, Subscribers, Export. Exporteer je lijst maandelijks naar CSV en sla die op. Je mailinglijst is waardevoller dan je website, behandel hem zo ook.
De Britt-check: voordat je live gaat
Na de checklist: laat Claude en ChatGPT nog een ronde over je code gaan. Drie prompts, in deze volgorde.
Die drie rondes halen gemiddeld 80 tot 90 procent van de beginnersfouten eruit. De rest is onderhoud.