MVP for buying and selling unique Lithuanian vehicle number plates.
- Next.js 15 (App Router, TypeScript, Server Actions)
- Tailwind CSS for styling
- Supabase (Postgres + Auth + RLS) for backend and database
- Vercel for hosting (planned)
-
Install dependencies:
npm install
-
Create a Supabase project at https://supabase.com and copy:
- Project URL
anonpublic keyservice_rolekey (server-only — never expose this in the browser)
-
Copy
.env.exampleto.env.localand fill in your keys:cp .env.example .env.local
-
Run the database migrations. In the Supabase dashboard, open SQL Editor and run, in order:
supabase/migrations/0001_initial_schema.sql— tables, RLS,public_profilesviewsupabase/migrations/0002_auth.sql— profile-creation trigger, OTP & rate-limit functions
-
Start the dev server:
npm run dev
This MVP runs its own OTP layer (no real SMS provider yet):
- Open
/prisijungti, enter a Lithuanian mobile (+370 6XX XXX XXX). - The 6-digit code prints to the server console — copy it from your
npm run devterminal. - Enter the code. On success you're redirected to
/profilis.
Rate limits in effect: 3 OTP requests / phone / hour, 5 / IP / hour, 10 verify attempts / phone / 15 min, max 5 attempts per code. Codes expire after 5 minutes.
To switch to a real SMS provider later, implement SmsProvider in src/lib/sms/, register it in getSmsProvider(), and set SMS_PROVIDER in your environment.
A user with profiles.is_admin = true can access /admin (the reports queue). To grant admin to yourself in dev:
update public.profiles set is_admin = true where phone = '+370XXXXXXXX';Non-admins get a 404 on /admin/* so the route's existence isn't disclosed. Admin actions (dismiss / resolve / remove) write an audit row to admin_actions.
Reports can target listings, wanted ads, or individual messages. Each (reporter, target) pair is unique at the DB level — one report per item per user.
The OTP request endpoint already calls getCaptchaProvider().verifyToken(token, ip). Today the dev stub accepts any token (gated on CAPTCHA_DEV_MODE=true). Wiring a real provider:
- Pick a provider — Cloudflare Turnstile (recommended) or hCaptcha.
- Add
CAPTCHA_TURNSTILE_SITE_KEY(public, can beNEXT_PUBLIC_*) andCAPTCHA_TURNSTILE_SECRET_KEY(server-only) to.env. - Implement
src/lib/captcha/turnstile.tsexporting aCaptchaProviderwhoseverifyTokendoes the HTTPS POST tohttps://challenges.cloudflare.com/turnstile/v0/siteverify. - Register it in
src/lib/captcha/provider.ts(case 'turnstile'). - Set
CAPTCHA_PROVIDER=turnstileandCAPTCHA_DEV_MODE=falsein production. - Add the widget script +
<div class="cf-turnstile" data-sitekey="…">to the sign-in page where the existing#captcha-mountdiv is. Read the token from the widget callback and send it in the OTP request body ascaptcha_token.
The seam is in place — only step 3 is real new code; everything else is config.
src/
├── app/ # Next.js routes (App Router)
├── lib/
│ ├── supabase/ # Supabase clients (browser, server, middleware)
│ ├── validation/ # Input validators (phone, etc.)
│ └── i18n/ # Lithuanian UI strings
└── middleware.ts # Session refresh on every request
supabase/
└── migrations/ # SQL schema migrations (run manually in dashboard for MVP)
- No secrets in client-side code; service role key is server-only.
- All write paths validated server-side (Zod schemas + RLS).
- Phone format strictly
+370(Lithuanian mobiles only). - Rate limiting on OTP, listing creation, and messaging.
- One phone number per account (DB unique constraint).
See ROADMAP.md.