Case Study

GliVal

A multi-tenant booking and payments platform for appointment-based businesses: v1 in production at glival.com, v2 a full rewrite on its way to replace it.

Period
– present
Role
Lead engineer; sole engineer of v2
  • 1,807

    Commits on v2, all mine

  • 1,268 (1,119 mine)

    Commits on v1

  • 46

    Architecture decision records (v2)

  • 9

    Languages (v2)

  • 326

    Test files (v2)

  • 4 days (19 to 23 September 2026)

    Python backend ported to Next.js

The glival.com home page: "Booking & Payments Platform for Service Providers", a Get Started (Free) button and the kinds of businesses it serves

A booking and payments platform in production, and the rewrite that will replace it

Problem

Appointment-based businesses such as beauty, wellness, coaching and home services take bookings in one tool, payments in another and keep a website in a third, and a client who books and does not come costs them the slot. GliVal gives each business its own booking page at its own address, takes a deposit or the full price by card when the client books, applies the cancellation and no-show rules the business sets, and pays out to the business's own Stripe account.

What was built

v1 is a Next.js, Prisma and PostgreSQL application with three interfaces: an admin for the platform, a portal for each business and booking pages for its clients. Payments run on Stripe Connect: the card is authorised when the client books, and a background worker captures it at a time computed from the appointment and the business's rules, so every cancellation case ends in a defined capture, release or refund. Clients are notified by email and SMS. Production runs as Docker Compose behind Traefik on a server provisioned with Terraform, with Cloudflare DNS for every business address and GitLab CI. I led v1 and wrote most of it; a second developer contributed to it.

v2 is a rewrite of the whole product that I am building alone. It started as a FastAPI backend behind a Next.js frontend; in September 2026 I decided to port the backend into one Next.js codebase, route by route behind a fallback to the Python process, and deleted the Python four days later. Drizzle owns the schema, a pg-boss worker runs jobs from the same PostgreSQL database, next-intl serves nine languages and configuration is one dotenvx-encrypted file per environment. Every money operation goes through one Payment module that records the intended action before it calls Stripe, so a retry resumes the same charge instead of making a new one. Each business also gets an editable website: sections with ready-made presets, a site theme with light and dark palettes, and its next free booking times read live. 46 decisions are written down as ADRs.

Outcome

v1 is in production at glival.com. v2 runs on staging, deployed by GitLab CI to my own VPS behind the shared Traefik, with a Vitest suite whose server tests run against an in-process Postgres. An audit of security and money paths is being worked through before release; then one maintenance window carries v1 data across and adopts each business's existing Stripe account instead of creating a new one. The source of both versions is private; glival.com is the evidence.

Shipping Timeline

From zero to production.

  1. 01 · Build

    v1 development starts

    Next.js, Prisma and PostgreSQL.

  2. 02 · Deploy

    v1 production infrastructure

    Terraform for the server and Cloudflare DNS, a production Docker Compose stack behind Traefik, GitLab CI.

  3. 03 · Support

    Database backup service in production

  4. 04 · Requirements

    v2: the rewrite begins

    A new repository, a new stack, the same product.

  5. 05 · Architecture

    Schema restarted from one baseline

    235 migration revisions replaced by one, keeping the triggers that make financial and audit tables append-only.

  6. 06 · Architecture

    Port to one Next.js codebase decided

    Route by route, behind a fallback to the Python process.

  7. 07 · Build

    Python backend deleted

    Four days after the decision; pg-boss in Postgres replaces Celery and Redis.

  8. 08 · Deploy

    Staging deployed by GitLab CI on my VPS

    Behind the shared Traefik, from the develop branch.

  9. 09 · Deploy

    Infrastructure in Terraform, applied by CI

    DNS records, Turnstile widgets and Google keys declared in code.

  10. 10 · Support

    Next: cutover from v1

    One maintenance window carries v1 data into v2 and adopts existing Stripe accounts.

Engineering

How it is built.

Stack

  • TypeScript
  • Python
  • React
  • Next.js
  • Tailwind CSS
  • Node.js
  • FastAPI
  • PostgreSQL
  • Prisma
  • Docker
  • GitLab CI/CD
  • Traefik
  • Terraform
  • Encrypted secrets (SOPS, dotenvx)
  • Vitest
  • Playwright
  • Stripe

Decisions

Authorise at booking, capture on schedule
In v1 a card payment is authorised when the client books and captured later by a background worker at a time computed from the appointment and the business's rules. Cancellations inside and outside the window, no-shows and approvals each have a defined path to capture, release or refund, so money never moves before the rules say it should.
One owner for every money operation
In v2 only the Payment module talks to Stripe. It registers each operation, its frozen request and its idempotency keys before the provider call, makes the call outside any database transaction and stores the observed result separately. A retry resumes the same action, and an unknown result never permits a charge with a fresh key.
Clients pay the business directly, and v1 accounts are adopted
In v2 a client's payment goes straight to the business's connected Stripe account, never through the platform balance; platform subscriptions are paid to GliVal separately. At cutover each v1 connected account is read back and adopted rather than recreated, so a business keeps one history of payments, refunds, disputes and payouts.
One Next.js codebase instead of Python and Next.js
The FastAPI backend was ported into Next.js route handlers at the same paths with the same payloads, so the frontend's fetch layer and the webhook endpoints registered with providers did not change. A fallback rewrite sent every path not yet ported to the Python process, which kept develop runnable until the last route moved. Drizzle was chosen over Prisma because the schema leans on JSONB, enums and triggers.
Jobs live in Postgres and are enqueued with the change
pg-boss stores jobs in the application's own database, so a job is enqueued in the same transaction as the domain change it follows. That replaced Celery, Redis and a transactional outbox with its recovery sweep, and the worker runs from the same image as the web process.