Skip to content
mouaad.sek:~$
All case files~/work/cf-04$
CF-04Product · Real businessSTATUS: DELIVERED

L'Adress restaurant website

A restaurant site the owners can run themselves, with forms that are validated, rate-limited and backed by row-level security.

ROLE
Solo, built for the restaurant
WHEN
2026
STACK
Next.js 15 App Router, Tailwind CSS 4, Supabase (Postgres, Auth, Storage), Resend, Docker, Vercel
LINK
Private repo, demo on request
01 / CONTEXT

The problem

A small restaurant needs more than a menu page: table reservations, event quotes, a photo gallery and news, all editable without calling a developer.

Public forms on a small business site are also the first thing bots find, so security had to be part of the build, not an afterthought.

02 / APPROACH

From a visitor form to the owner inbox

  1. 01Visitor form
  2. 02Zod validation
  3. 03Rate limiter
  4. 04Supabase + RLS
  5. 05Email to owner
03 / SECURITY BUILT IN

Security built in

CONTROLLAYERPROTECTS
Row-level security policies on every tableDatabaseData access
Admin area behind authentication middlewareAppBack office
Schema validation on every form inputAPIInjection, junk
Rate limiting on public endpointsAPISpam, abuse
Service-role key kept server-onlySecretsKey exposure
Content-Security-Policy and security headersHTTPClickjacking, XSS
04 / EVIDENCE

Straight from the code

Excerpts from the actual repository and deliverables, trimmed for length.

supabase/migrations/001_initial_schema.sqlsql
ALTER TABLE public.reservations ENABLE ROW LEVEL SECURITY;

CREATE POLICY "public can insert reservations"
  ON public.reservations FOR INSERT
  TO anon, authenticated
  WITH CHECK (true);

CREATE POLICY "admins can manage reservations"
  ON public.reservations FOR ALL
  TO authenticated
  USING (is_admin());

Row-level security: visitors can only insert a reservation, never read one. Reading and managing is reserved to admins.

src/lib/rate-limit.tstypescript
import { RateLimiterMemory } from 'rate-limiter-flexible'

// NOTE: In-memory limiters reset on cold starts in serverless environments.
// For production, swap to RateLimiterRedis or RateLimiterPostgres.

const limiters = {
  reservation: new RateLimiterMemory({ points: 5, duration: 3600 }),
  devis: new RateLimiterMemory({ points: 3, duration: 3600 }),
  contact: new RateLimiterMemory({ points: 5, duration: 3600 }),
} as const

Per-endpoint rate limits on the public forms. The comment flags the known limitation, picked up in the lessons below.

05 / OUTCOME

Results

11public pages, French, SEO-ready
16row-level security policies
3public form flows with owner emails
06 / LESSONS

What I'd do differently

Clients do not ask for row-level security, but they suffer when it is missing. Next: replace the in-memory rate limiter with a shared store so it survives scaling.

NEXT CASE FILE · CF-05Canritco infrastructure audit
Esc

↑↓ navigate · Enter open · Esc close