Dutch consumer protection. Paste a webshop URL → get a verifiable, evidence-based report covering KVK identity, BW 6 disclosure obligations, BTW validity, withdrawal rights, and operator clustering. Facts, not verdicts. Public methodology. Open source.
The full specification lives under docs/. Start with
docs/product-spec.md for the mission and
docs/principles.md for the ten non-negotiable rules.
Status. Slice 1 (foundation) is complete: the end-to-end pipe works against a local Supabase stack, with one rule shipped (
NL-IDENT-004— KVK deregistered). Slice 2 expands to the full NL identity/contact/VAT ruleset. The roadmap lives indocs/phases/.
Prerequisites: Node 24, pnpm 11, Docker Desktop (for Supabase local stack).
pnpm install
cp .env.example .env.local # fill in the keys after `pnpm supabase status`# Terminal 1 — local Supabase stack (Postgres, Realtime, Storage, Studio)
pnpm dev:db:start
pnpm dev:db:status # copy NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY +
# SUPABASE_SECRET_KEY into .env.local
# Terminal 2 — apply migrations once
pnpm db:migrate
# Terminal 3 — scan worker poll loop (replaces cloud pg_cron for local dev)
pnpm dev:fn:loop
# Terminal 4 — Next.js dev server
pnpm dev # http://localhost:3000Submit a webshop URL (or paste https://example-shop-a.test in NEXT_PUBLIC_DEMO_MODE=true)
on the landing page. Live progress streams via Supabase Realtime; the report renders
at /r/<scan-id> once the worker completes the pipeline.
End-to-end verification without the UI:
node --import tsx scripts/smoke-test.tsInserts a scan for the deregistered KVK fixture (12345671), enqueues it, runs
the worker, and asserts the resulting traffic light is red with one
NL-IDENT-004 critical finding.
Proves the public-read policies on scans and findings work for unauthenticated
visitors (prerequisite: dev server running):
node scripts/anon-read-check.mjs| Script | What it does |
|---|---|
pnpm dev |
Next.js dev server with Turbopack |
pnpm dev:db:start / dev:db:stop / dev:db:reset |
Manage the local Supabase Docker stack |
pnpm dev:db:status |
Prints local URL + sb_publishable_* / sb_secret_* keys |
pnpm dev:fn:loop |
Poll the scan worker every 5s (local pg_cron replacement) |
pnpm dev:fn:tick |
Run the worker once and exit |
pnpm db:generate |
Generate Drizzle SQL migration from lib/db/schema.ts |
pnpm db:migrate |
Apply pending migrations |
pnpm db:studio |
Open Drizzle Studio against the local DB |
pnpm db:verify-rls |
Assert every table has RLS + ≥1 policy |
pnpm typecheck |
tsc --noEmit strict |
pnpm lint |
ESLint, includes banned-vocabulary plugin |
pnpm test |
Vitest |
User pastes URL
→ Server Action submitScan (app/actions/submit-scan.ts)
→ INSERT scans row, pgmq.send('scan_queue', { scanId })
→ /scan/[id] page (Realtime subscription)
Local worker loop (scripts/run-worker.ts, replaces cloud Edge Function):
pgmq.read → runScan → write findings → update scans.status='complete'
/r/[id] page (Server Component):
read scans + findings → render report → SEO meta + OG image
Tech stack: Next.js 16 (App Router, proxy.ts not middleware.ts) ·
React 19.2 · Drizzle 1.0-rc with defineRelations (RQB v2) ·
Supabase SSR 0.10 with new sb_publishable_* / sb_secret_* keys
(legacy JWT anon / service_role keys are not used) ·
Tailwind v4 (CSS-first config) · next-intl 4
(defineRouting + createNavigation) · Vitest 4 + Vite 7.
Full version table and rationale: docs/architecture.md.
- Facts, not verdicts.
- No confidence scores — count-based traffic light only.
- Severity is fixed per rule.
- Verification beats extraction. Never assume.
- Targeting determines applicable law.
- Layered presentation (glance / findings / raw evidence).
- Reproducibility — every scan stamps scanner-version + ruleset-version.
- Cluster awareness as a feature.
- Transparency over cleverness.
- Banned vocabulary (
scam,fraud,fake,dodgy,dropshipper, …) — enforced by ESLint + runtime rule lint.
Full text: docs/principles.md.
Code: AGPL-3.0-or-later (LICENSE-AGPL).
Rule definitions exported from lib/rules/: Apache-2.0 (LICENSE-APACHE).
Documentation under docs/: CC-BY-4.0 (LICENSE-DOCS).