Skip to content

gussttaav/personal-tutoring-platform

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

327 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GUSTAVOAI.DEV

Personal tutoring platform for booking programming, mathematics and AI classes.

Live site: gustavoai.dev

Next.js TypeScript Supabase Stripe Zoom Vercel License


Overview

Full-stack booking platform for online tutoring sessions. Students can schedule individual sessions or purchase class packs, pay securely inside the app, join live virtual classrooms embedded in the platform, and manage all their bookings from a personal dashboard. An AI assistant powered by Gemini answers questions about services, pricing, and scheduling around the clock. The customer-facing experience is fully bilingual (Spanish / English) with automatic browser-language detection and a manual switcher.


Tech Stack

Technology Purpose
Next.js 16 (App Router) Full-stack framework; RSC for static sections, client components only where interactivity is needed
TypeScript (strict) End-to-end type safety
NextAuth v5 Google OAuth authentication
next-intl Internationalization: Spanish (default) + English under /en, Accept-Language auto-detection, localized UI, emails, and SEO metadata
Supabase (Postgres) Source of truth for all persistent data: users, bookings, credit packs, payments, audit log
Stripe Integrated payment forms (single sessions and packs); webhook processing for payment confirmation
Google Calendar API Reads real-time availability; creates and deletes calendar events on booking/cancellation
Zoom Video SDK Embedded virtual classroom inside the platform; no external app or install required
Upstash Redis Ephemeral state only: rate limiting, slot locking, availability cache, in-session chat state
Gemini API AI assistant trained on full service details, pricing, and cancellation policy
Resend Transactional email: booking confirmations, cancellation notices, reschedule links
Sentry Error tracking and monitoring in production
Jest Unit and integration tests
Playwright End-to-end tests
GitHub Actions CI: runs the full test suite automatically on every push
Vercel Deployment and hosting

Features

  • Free intro session — 15-minute no-cost meeting to define a plan, no commitment required
  • Individual sessions — 1h and 2h paid sessions; payment handled inside the app via an integrated Stripe form
  • Class packs — buy 5 or 10 classes at a discount; credits are activated immediately after payment and last 6 months
  • Real-time availability — weekly calendar fetches free slots from Google Calendar on demand; slots are soft-locked during checkout to prevent double-booking
  • Embedded virtual classroom — sessions run in a Zoom-powered room inside the platform; no install, no redirect
  • Personal dashboard — students see all their upcoming and past sessions and can join, reschedule, or cancel from one place
  • Email notifications — every booking triggers a confirmation email with calendar link, join link, and one-click reschedule/cancel links
  • AI assistant — Gemini chat widget answers questions about services, pricing, cancellation policy, and Gustavo's background without the student needing to send an email
  • Bilingual (Spanish / English) — Spanish is the default at unprefixed URLs; English lives under /en. First-time visitors are auto-routed by browser language, and a navbar switcher lets anyone change language on the fly (persisted in a cookie). The full customer UI and transactional emails are translated; the admin panel stays Spanish
  • Automatic session closing — a pending termination row is written at booking time; a daily cron sweeps and closes virtual rooms once their grace period has elapsed
  • Google OAuth — sign-in required before booking; session is verified server-side on all API routes

Architecture

The codebase follows a strict layered architecture. Route handlers are thin dispatchers; all business logic lives in the service layer.

src/
├── app/            Route handlers — parse input, call service, format response
├── domain/         Pure types, interfaces, domain errors (zero external deps)
├── services/       Business logic — orchestrates repositories and infrastructure
├── infrastructure/ External adapters — Supabase, Stripe, Google, Zoom, Resend, Redis
├── lib/            Shared utilities — schemas, validation, logger, rate limiting
├── components/     Reusable React components
├── features/       Feature-scoped page components
├── hooks/          React hooks
└── constants/      Static configuration and design tokens

Data flow:

Route handler → Service → Repository interface → Supabase implementation
                                              └→ In-memory implementation (tests)

Key design decisions:

  • Repository pattern — all data access goes through interfaces in src/domain/repositories/. Services receive repositories via constructor injection, making them fully testable with in-memory fakes without mocking.
  • Redis is ephemeral only — Supabase is the single source of truth for all persistent data. Redis handles rate limiting keys, slot locks, and short-lived availability cache. Nothing in Redis matters after a page refresh.
  • Credit atomicity — pack credit decrements use a Postgres stored procedure (decrement_credit) to prevent race conditions under concurrent requests.
  • Thin route handlers — handlers parse and validate input with Zod, call one service method, and map domain errors to HTTP responses via a central error-mapping utility. No business logic in routes.
  • Serverless-safe session cleanup — every booking writes the session's grace-period deadline to a pending_terminations table. A daily cron (/api/internal/session-cleanup) sweeps rows whose deadline has passed and terminates the corresponding Zoom session records. Failure to write the row is non-fatal and does not fail the booking.
  • Locale-aware routing & SEOnext-intl middleware handles Accept-Language detection and the /en prefix (localePrefix: 'as-needed', so Spanish stays unprefixed). All customer-facing copy lives in messages/{es,en}.json and renders through t() — no hardcoded UI strings. Domain and validation errors travel as codes, translated at the presentation boundary. Public pages emit per-locale hrefLang/canonical tags (src/lib/hreflang.ts), and a locale-aware sitemap.ts + robots.ts keep both language variants correctly indexable.

Security

Security is treated as a first-class concern throughout the codebase:

  • Authentication — Google OAuth via NextAuth v5. Session is verified server-side on every API route; no URL parameter trust.
  • CSRF protection — all state-mutating POST routes validate the Origin header via isValidOrigin(). The sole exception is the Stripe webhook, which uses its own HMAC signature verification.
  • Webhook signature verification — Stripe webhooks are verified with HMAC signatures before any processing occurs. The internal session-cleanup cron is protected by a CRON_SECRET bearer token.
  • Input validation — all external input (request bodies, query params) is validated with Zod schemas defined in src/lib/schemas.ts. Inline validation in route handlers is not permitted.
  • Tamper-proof action tokens — cancellation and reschedule links in emails use HMAC-SHA256 signed tokens. Tokens are single-use and expire, preventing replay attacks.
  • Rate limiting — sliding-window rate limits (Upstash Redis) protect chat, availability, checkout, and credit endpoints against abuse.
  • Admin routes — protected by a server-side isAdmin() check independent of the student auth flow.
  • No sensitive data in Redis — all persistent student and payment data lives in Supabase (Postgres), not in the cache layer.
  • Stripe payment security — card data is handled entirely by Stripe's embedded Elements; no card numbers touch the application server.
  • Error tracking — Sentry captures and alerts on unhandled exceptions in production without exposing stack traces to the client.

Testing & CI

pnpm test                # all Jest tests (unit + integration)
pnpm test:unit           # unit tests only
pnpm test:integration    # integration tests only
pnpm test:e2e            # Playwright end-to-end tests (requires E2E_BASE_URL)
  • Unit tests — service logic tested with in-memory repository fakes; no real network calls.
  • Integration tests — cover API route behaviour end-to-end within the Next.js request cycle.
  • E2E tests — Playwright tests cover the full booking and payment flows in a real browser.
  • GitHub Actions — the full Jest suite runs automatically on every push and pull request.

Local Setup

Prerequisites

  • Node.js 22+
  • pnpm 10+ (run corepack enable to get the version pinned in package.json)
  • Supabase project (free tier is sufficient)
  • Upstash Redis database (free tier is sufficient)
  • Stripe account (session/pack prices are managed in-app, not in Stripe)
  • Google Cloud project with Google Calendar API enabled and a service account
  • Zoom app with Video SDK credentials
  • Resend account
  • Sentry project (optional for local dev)

1. Clone and install

git clone https://github.com/gussttaav/personal-web-booking-app.git
cd personal-web-booking-app
pnpm install

2. Environment variables

Create .env.local in the project root:

# ── Auth ──────────────────────────────────────────────────────────────
AUTH_SECRET=                        # openssl rand -hex 32
AUTH_GOOGLE_ID=                     # Google OAuth client ID
AUTH_GOOGLE_SECRET=                 # Google OAuth client secret

# ── Supabase ──────────────────────────────────────────────────────────
SUPABASE_URL=https://xxxx.supabase.co
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

# ── Stripe ────────────────────────────────────────────────────────────
# Session/pack prices are NOT configured in Stripe — they live in the
# Supabase `pricing` table and are edited from the admin panel (/admin/pricing).
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# ── Upstash Redis (rate limiting + availability cache) ────────────────
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...

# ── Google Calendar (service account) ─────────────────────────────────
GOOGLE_SERVICE_ACCOUNT_EMAIL=xxx@xxx.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n"
GOOGLE_CALENDAR_ID=your.email@gmail.com

# ── Zoom Video SDK ─────────────────────────────────────────────────────
ZOOM_SDK_KEY=
ZOOM_SDK_SECRET=

# ── AI assistant ──────────────────────────────────────────────────────
GEMINI_API_KEY=

# ── Resend (transactional email) ──────────────────────────────────────
RESEND_API_KEY=re_...
RESEND_FROM=Gustavo Torres <contacto@gustavoai.dev>
NOTIFY_EMAIL=your.email@gmail.com

# ── Cancellation / Rescheduling tokens ───────────────────────────────
CANCEL_SECRET=               # openssl rand -hex 32

# ── Internal crons (cron-job.org → /api/internal/session-cleanup, /api/internal/reconcile-stripe) ──
CRON_SECRET=                 # openssl rand -hex 32

# ── Sentry ────────────────────────────────────────────────────────────
SENTRY_DSN=

# ── App ───────────────────────────────────────────────────────────────
NEXT_PUBLIC_BASE_URL=http://localhost:3000

3. Run in development

pnpm dev
# http://localhost:3000

4. Test Stripe webhooks locally

stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# Copy the whsec_... shown and set it as STRIPE_WEBHOOK_SECRET

Deployment

The project is deployed on Vercel. Set all environment variables from .env.local in the Vercel project settings.

Stripe webhook (production)

  1. Stripe Dashboard → Developers → Webhooks → Add endpoint
  2. URL: https://gustavoai.dev/api/stripe/webhook
  3. Events: checkout.session.completed, charge.refunded
  4. Copy the signing secret → set as STRIPE_WEBHOOK_SECRET in Vercel

Google Cloud

  1. Enable Google Calendar API in your project
  2. Create a Service Account → copy client_email and private_key
  3. In Google Calendar → your calendar → Settings → Share with specific people → add the service account email with "Make changes to events" permission
  4. Set GOOGLE_CALENDAR_ID to your Gmail address

Database

Run migrations after deploying schema changes:

supabase db push
# or apply migration files in supabase/migrations/ via the Supabase dashboard

License

MIT — see LICENSE.


Contact

Gustavo Torres Guerrero
gustavoai.dev · LinkedIn · GitHub · contacto@gustavoai.dev

About

Full-stack tutoring platform — book individual sessions or class packs, pay via Stripe, join embedded Zoom classrooms, and get instant answers from an AI assistant.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Contributors