Skip to content
Case study · 15 / 19← All work

Plug Club

A private club in Mar del Plata: barbershop bookings, monthly memberships with automatic billing and a DJ booth rented by the hour.

Role
Full-stack development for a client
Stack
Next.js (App Router), TypeScript, Prisma + Postgres, Auth.js, MercadoPago (Checkout + subscriptions), Tailwind CSS, Resend + Twilio, Vercel
Admin dashboard
Admin dashboardOne wallet with a ledger
(01) The problem

The club started as a barbershop that took bookings over WhatsApp. The owner wanted members who pay every month, get a set number of haircuts and booth hours, and can use them for guests. Non-members still had to be able to book and pay for a single haircut or an hour in the booth. Payments go through MercadoPago, and its notices don't always arrive.

My roleI was the only developer, from the data model to the deploy, working directly with the owner.
(02) ResultsCounted in the code
25Prisma models: bookings, payments, plans, memberships, wallet and the booth
70API handlers, each checked for its own permissions
31pages across the public site, the member account, the team panel and admin
(03) Architecture

Three decisions that shaped it.

  1. 01

    One wallet with a ledger

    Why. Each member has haircuts and booth hours from the monthly plan, plus loose credits that don't expire. Every booking takes from the plan first and then from credits, inside one transaction, and writes a row to the ledger: renewal, credit, use, guest use or manual adjustment. A booth booking can mix both: hours from the wallet and the rest paid, with the hours taken only when MercadoPago approves the payment.

    Trade-off. Memberships expire when they are read, not with a scheduled job. That keeps the deploy simple, but every query that counts active members has to repeat the end-date filter.

  2. 02

    MercadoPago is the source of truth, not its notices

    Why. Paid members were left without a membership because the webhook rejected every notice: the signing secret on the server was not the integration's. Now the webhook reads each payment or subscription again from the MercadoPago API, and a reconciliation step finds subscriptions by our own reference (user and plan). It runs when the member comes back from checkout and from a "Sync MP" button in the panel.

    Trade-off. A notice with a bad signature is logged and still processed. A fake one can only make us reprocess our own data, which is idempotent, but it is a weaker check than rejecting it.

  3. 03

    Static pages, permissions in every handler

    Why. The admin panel stopped rendering on the server. Its pages are prerendered and the middleware asks for the admin role before they load. Public pages are cached for a month and the panel invalidates them when the owner saves a change. Since the pages no longer check the session, each API handler checks its own permissions.

    Trade-off. Permissions are checked twice, in the middleware and in the handler, which is more code to keep in step. A change made outside the panel, straight in the database, doesn't show on the public site until the cache expires.

(04) GalleryKeep scrolling →
Admin dashboard
Admin dashboard · Bookings, income, active members, recurring revenue and how much of the wallets was used this month. Local run with sample data.
Membership plans
Membership plans · The owner sets price, haircuts, booth hours, booking priority and benefits per plan. Saving a plan creates its subscription in MercadoPago.
Booth settings
Booth settings · Hourly price for non-members, a separate price for members' extra hours, and opening hours per day.
Booking the booth on a phone
Booking the booth on a phone · A member sees the hours left in the wallet. They are used first; only the hours that don't fit are paid.
(05) What I'd do next
01Write tests for the wallet and the webhook: order of use, mixed booth payments and repeated MercadoPago notices. The repo has none today.
02Load the right signing secret and go back to rejecting notices with a bad signature.
03Move schema changes back to Prisma migrations. The repo has only the first migration, and the membership, wallet and booth tables are not in it.
Next project · 16 / 19
La Trencería →Bookings, shop, courses and loyalty for an afro-braiding studio
Case study · 15 / 19

Plug Club

A private club in Mar del Plata: barbershop bookings, monthly memberships with automatic billing and a DJ booth rented by the hour.

Role
Full-stack development for a client
Stack
Next.js (App Router), TypeScript, Prisma + Postgres, Auth.js, MercadoPago (Checkout + subscriptions), Tailwind CSS, Resend + Twilio, Vercel
Plug Club
(01) The problem

The club started as a barbershop that took bookings over WhatsApp. The owner wanted members who pay every month, get a set number of haircuts and booth hours, and can use them for guests. Non-members still had to be able to book and pay for a single haircut or an hour in the booth. Payments go through MercadoPago, and its notices don't always arrive.

My roleI was the only developer, from the data model to the deploy, working directly with the owner.
(02) Results
25Prisma models: bookings, payments, plans, memberships, wallet and the booth
70API handlers, each checked for its own permissions
31pages across the public site, the member account, the team panel and admin
(03) Architecture

Three decisions that shaped it.

01

One wallet with a ledger

Why. Each member has haircuts and booth hours from the monthly plan, plus loose credits that don't expire. Every booking takes from the plan first and then from credits, inside one transaction, and writes a row to the ledger: renewal, credit, use, guest use or manual adjustment. A booth booking can mix both: hours from the wallet and the rest paid, with the hours taken only when MercadoPago approves the payment.

Trade-off. Memberships expire when they are read, not with a scheduled job. That keeps the deploy simple, but every query that counts active members has to repeat the end-date filter.

02

MercadoPago is the source of truth, not its notices

Why. Paid members were left without a membership because the webhook rejected every notice: the signing secret on the server was not the integration's. Now the webhook reads each payment or subscription again from the MercadoPago API, and a reconciliation step finds subscriptions by our own reference (user and plan). It runs when the member comes back from checkout and from a "Sync MP" button in the panel.

Trade-off. A notice with a bad signature is logged and still processed. A fake one can only make us reprocess our own data, which is idempotent, but it is a weaker check than rejecting it.

03

Static pages, permissions in every handler

Why. The admin panel stopped rendering on the server. Its pages are prerendered and the middleware asks for the admin role before they load. Public pages are cached for a month and the panel invalidates them when the owner saves a change. Since the pages no longer check the session, each API handler checks its own permissions.

Trade-off. Permissions are checked twice, in the middleware and in the handler, which is more code to keep in step. A change made outside the panel, straight in the database, doesn't show on the public site until the cache expires.

(04) Gallery
Admin dashboard
Admin dashboard · Bookings, income, active members, recurring revenue and how much of the wallets was used this month. Local run with sample data.
Membership plans
Membership plans · The owner sets price, haircuts, booth hours, booking priority and benefits per plan. Saving a plan creates its subscription in MercadoPago.
Booth settings
Booth settings · Hourly price for non-members, a separate price for members' extra hours, and opening hours per day.
Booking the booth on a phone
Booking the booth on a phone · A member sees the hours left in the wallet. They are used first; only the hours that don't fit are paid.
(05) What I'd do next
01Write tests for the wallet and the webhook: order of use, mixed booth payments and repeated MercadoPago notices. The repo has none today.
02Load the right signing secret and go back to rejecting notices with a bad signature.
03Move schema changes back to Prisma migrations. The repo has only the first migration, and the membership, wallet and booth tables are not in it.
Next projectLa Trencería →
Nahuel SantillánEN