Elena Bonciarelli

Website and booking for a psychology practice

Marketing site and online booking system for the Il Giardino di Psiche psychology practice in Ardea. A single Next.js app serves both pages and APIs: clients book a session by choosing mode, location and time, and every appointment creates an event on the specialist's Google Calendar with confirmation emails. It includes an admin area with OTP login to manage availability and announcements.

Year
2026
Role
Full-stack development
Status
In production
Client
Il Giardino di Psiche
TypeScriptNext.jsReactTailwind CSSSQLiteGoogle Calendar APINodemailerDocker

Overview

A single Next.js application serves both the practice's public pages (intro, team, services, locations, FAQ) and the booking APIs, plus a private admin area. In production a booking creates a real event on the specialist's Google Calendar and sends emails to client and practice.

Problem

The practice needed clients to book sessions online without any risk of double booking, keeping the agenda in sync with the tool the specialists already use — Google Calendar — and without adding a separate backend or a complex admin panel for a non-technical person to learn.

Architecture

A pnpm monorepo: the Next.js web app (App Router, React, strict TypeScript) and a package of Zod schemas shared between client and server, so validation rules are written once. Site and APIs run in the same process: no separate service, no CORS.

The server is three layers — controllers (validation + HTTP errors), services, repositories. Persistence is SQLite (better-sqlite3, WAL) on a Docker volume.

Browser ──► Next.js (pages + /api)
                │
        controller ─► service ─► repository ─► SQLite
                │
                ├─► Google Calendar API  (1 service account, N calendars)
                └─► SMTP (Nodemailer)

Implementation

Practice content is stored as versioned JSON files, a single source of truth, exposed read-only through the API. Booking is a stepped flow (mode → location → day/time → contacts): availability is computed by combining Google Calendar events, local bookings and manual admin blocks.

POST /api/bookings applies a per-IP rate limit, re-validates the slot, writes to SQLite, then creates the Calendar event and sends the emails non-blockingly: if those fail the booking still stands and the error is only logged.

Design decision

A partial unique index on (psy_id, date, time, mode) WHERE status='active' makes double booking impossible at the database level, with no application locks.

The admin area has no password: 6-digit OTP login by email, an address whitelist managed from the interface itself, a 30-day cookie session. The availability manager is a weekly calendar where every action applies immediately through the API, with no "Save" button — built for a non-technical user.

Challenges

  • Concurrency on the same slot: solved with a database uniqueness constraint rather than locks.
  • Dependency on an unreliable external service: availability falls back to local bookings only when Google Calendar is unreachable.
  • Calendar webhooks with no scheduler: Google channels expire in about a week, so they are renewed lazily, triggered by traffic, plus a protected manual endpoint.

Results

Live at elenabonciarelli.com, served by Next.js behind Cloudflare. End-to-end booking works, with two-way sync to the specialist's calendar and email notifications. Reproducible deploy with a single docker compose up -d --build.

Lessons learned

Push invariants into the database instead of application code, design external integrations as best-effort, and match the admin interface to the actual technical level of the person using it: immediate feedback, no "Save", the same visual language as the public site.