- TypeScript 98.1%
- CSS 1.2%
- PLpgSQL 0.3%
- Dockerfile 0.3%
| .claude | ||
| app | ||
| components | ||
| deploy | ||
| lib | ||
| LOGO | ||
| prisma | ||
| public/brand | ||
| var/uploads | ||
| .aiexclude | ||
| .cursorignore | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| Caddyfile | ||
| CLAUDE.md | ||
| DEPLOY.md | ||
| DESIGN.md | ||
| docker-compose.override.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| PRODUCT.md | ||
| proxy.ts | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
LightOnNails
LightOnNails is the complete website for a nail salon, live at lightonnails.online. It covers everything a client needs — browsing services, booking an appointment, paying online, joining the VIP club, sharing nail photos — plus a full admin panel for the salon owner. The entire interface is in French, with a bright summer theme and a gold "luxe" variant for VIP members.
The public site
Home — hero section with animated summer nails, feature highlights, latest news, and structured data (JSON-LD) for local-business SEO.
Prestations — the service menu: nails, nail art, and foot care, each with duration and price (VIP prices shown when applicable).
Réservation — a step-by-step booking wizard: pick a service, pick a one-hour slot on the calendar, confirm. Slots are overbooking-proof, and a waitlist is available when a day is full. Payment can be made online by card, from the wallet, or at the salon. A confirmation page and email follow every booking.
Cartes cadeaux — buy a gift card for someone: choose the amount, the recipient receives a code by email and credits it to their wallet.
Club VIP — memberships of 1, 3, 6, 9, or 12 months (prepaid or auto-renewed) with preferential prices, priority booking, exclusive news, and a gold luxury theme across the site.
Galerie — a community gallery: clients upload photos of their nails, everyone votes with likes, and a "creation of the month" ranking runs monthly (with an all-time top filter). Submissions are moderated before publication.
Avis — verified client reviews (only after a completed appointment), with star ratings and review structured data for search engines.
Actualités — news, promotions, and behind-the-scenes posts written in Markdown, each either public or VIP-only.
Newsletter — subscription with double opt-in confirmation and one-click unsubscribe.
Legal pages — CGU, CGV, mentions légales, and privacy policy, all editable from the admin panel.
Client accounts
- Sign-up with email verification, login, password reset, and optional two-factor authentication (TOTP).
- Mon espace — upcoming and past appointments, with the ability to reschedule up to 24 h before the slot (no client-side cancellation; inside 24 h the appointment is due).
- Portefeuille — a wallet credited by codes or gift cards and usable for any payment on the site, with full transaction history.
- Sécurité — password change and 2FA setup.
- Mes données — GDPR tools: export all personal data or delete the account.
Admin panel
A dedicated /admin area for the salon owner: dashboard, statistics with charts, appointment management, client records, services, opening hours and closed dates, secure code and gift-card generation, VIP member management, news publishing, newsletter sending, gallery moderation, review moderation, payment overview, audit log, legal-page editing, and site settings (branding, business info).
Design & SEO
Responsive, mobile-first layout; summer color palette with the VIP gold variant; French-language UI throughout; per-page metadata, JSON-LD (local business + reviews), sitemap.xml, and robots.txt. Visitor analytics use a cookieless, GDPR-friendly Setka (Umami) tag that loads only on the production build.
Local development
Prerequisites: Node 22 and Docker (for Postgres).
# 1. Install dependencies
npm install
# 2. Configure the environment
cp .env.example .env
# then edit .env — at minimum set POSTGRES_* / DATABASE_URL and generate the
# three secrets (the command is at the top of .env.example). For local testing
# without Stripe/SMTP you can leave those blank.
# 3. Start only the database (the app runs on the host in dev)
docker compose up -d db
# 4. Apply migrations and seed (idempotent)
npm run db:migrate
npm run db:seed
# 5. Run the dev server
npm run dev # http://localhost:3000
Handy scripts: npm run typecheck, npm run lint, npm run db:studio, npm run db:reset.
The seed creates the first administrator. In development it defaults to
admin@lightonnails.fr / ChangeMe!2026 (override with SEED_ADMIN_EMAIL /
SEED_ADMIN_PASSWORD). In production the seed refuses the default and requires
a strong SEED_ADMIN_PASSWORD — change it from /admin after first login.
Hosting & production
The whole stack — app, Postgres, migrations, cron scheduler, and a Caddy HTTPS
front-end — ships in docker-compose.yml. Caddy automatically obtains and renews a
Let's Encrypt certificate for the domain, so no manual TLS setup is required.
Prerequisites: a Linux server with Docker and Docker Compose, ports 80 and 443 open to the internet, and the domain's DNS A record (and AAAA if you have IPv6) pointing at the server's public IP.
First deploy
# 1. Clone the repo onto the server, then create the environment file
cp .env.example .env
# 2. Edit .env and set, at minimum:
# - POSTGRES_PASSWORD (a strong database password)
# - SESSION_SECRET, CODE_HMAC_SECRET, CRON_SECRET
# each: node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
# - DOMAIN=lightonnails.online and LETSENCRYPT_EMAIL=you@example.com
# - APP_URL / NEXT_PUBLIC_APP_URL = https://lightonnails.online
# - SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD (strong; used for the first /admin login)
# - DATABASE_URL — keep the lon:… host part as "localhost" only for host-side CLI;
# the containers build their own DATABASE_URL from POSTGRES_* automatically.
# 3. Build and launch the full stack (migrations + seed run once, then the app)
docker compose up -d --build
# 4. Watch it come up
docker compose ps
docker compose logs -f caddy # first cert issuance appears here
Once DNS resolves and ports 80/443 are reachable, https://lightonnails.online serves
over HTTPS with www redirecting to the apex. Log in at /admin with the seed admin
credentials and change the password immediately.
Notes
- Runtime source of truth for URLs is
APP_URL(links, emails, sitemap, Stripe redirects) — alwayshttps://lightonnails.onlinein production. TRUSTED_PROXY_HOPS=1matches the single bundled Caddy proxy, so rate limiting sees the real client IP. Change it only if you add more proxies in front.- Bring your own proxy? If you already run Traefik / Nginx Proxy Manager, delete the
caddyservice fromdocker-compose.ymland point your proxy at the app on127.0.0.1:3000. - Stripe (optional): webhook endpoint
https://lightonnails.online/api/stripe/webhook; enable exactly these events:checkout.session.completed,checkout.session.async_payment_succeeded,invoice.paid,customer.subscription.deleted. Without Stripe keys, online card payment is hidden but the wallet/code and pay-at-salon options still work. - SMTP (recommended): without it, transactional emails — booking confirmations,
password resets, gift-card codes, reminders — are logged instead of sent. Set the
SMTP_*vars before real launch so password reset and confirmations actually reach clients. Two mailboxes are supported on the same server:SMTP_USER(no-reply@) for transactional mail and an optionalSMTP_NEWSLETTER_USER(newsletters@) for marketing broadcasts — each authenticates as itself so theFrommatches. - Uploaded images persist in the
uploadsvolume; business data indb_data; TLS certificates incaddy_data. Back these up (see below) — losingcaddy_datajust re-issues certs, but losingdb_data/uploadsloses data. SESSION_SECRETandCODE_HMAC_SECRETare required (≥16 chars — the app refuses to start otherwise), as is a strongSEED_ADMIN_PASSWORD.
Backups
Both volumes should be backed up regularly:
# Database (nightly recommended)
docker compose exec -T db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backup-$(date +%F).sql
# Uploaded images
docker run --rm -v lightonnails_uploads:/data -v "$PWD":/backup alpine \
tar czf /backup/uploads-$(date +%F).tar.gz -C /data .
Restore the database with psql, and the uploads by extracting the tarball back into the volume.
2FA lockout recovery (admin)
If the sole administrator loses their authenticator, clear 2FA directly in the database, then log in and re-enroll:
docker compose exec db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c "UPDATE \"User\" SET \"totpSecret\"=NULL, \"totpEnabledAt\"=NULL WHERE email='admin@lightonnails.online';"