cat jnaninyoga-command-center.pwa
05 / 19
Jnanin Yoga Command Center
The studio's back office: schedule, check-ins, a credits wallet per student, health alerts, B2B invoicing.
- role
- sole developer
- year
- 2026
- access
- -rwxr-xr-x (open source)
- problem
- One instructor runs the whole studio: group classes, private sessions, outdoor retreats, and hotels and companies that book groups. She needed one place for all of it, on her phone.
- approach
- Google stays the source of truth for time and people: every session is a Calendar event, every client a Google contact. The app keeps only what Google can't: credit wallets, the attendance ledger, health logs, and the B2B paperwork.
- outcome
- One installable app for the week, check-in with health guardrails, prepaid credits per student, and quote-to-invoice billing that prints French invoices, from the quote to the last payment.
TL;DR

// overview
An installable, mobile-first admin app for a solo yoga instructor. Students buy credits, not dates: each check-in spends one and a missed class costs nothing. Before anyone is checked in, an active health condition raises an alert, from asthma to a due date.
Google Calendar stays the source of truth for group classes and private sessions, and every new client lands in Google Contacts, so they show up in WhatsApp too. Outdoor events keep track of their participants and guests.
A B2B side serves partner companies: quotes turn into invoices, partially if need be with a backorder for the rest, payments are tracked, and every document prints to PDF.
// the door
There is one user, the instructor, and she signs in with Google. The same consent lets the app read and write her Calendar and her Contacts, the two things everything else is built on.
Google doesn't always hand the refresh token back through the auth provider, so the app keeps its own copy, refreshes it five minutes before it expires, and only sends her back to sign in when Google says no. It installs from the browser like any app, and says so when the connection drops and again when it's back.
// the week
Sessions live in her Google Calendar, not in a database. Each event carries its type in private properties Google keeps for the app: group, private, outdoor or B2B, with a price where it matters, and each type gets a colour of its own in the calendar.
Creating a session checks three things before it reaches Google: it isn't in the past, a studio class fits the opening hours set for that weekday, and Google's FreeBusy finds no clash. Outdoor and B2B sessions happen elsewhere, so they skip the opening hours.
// credits, not dates
A card of five or ten classes, or a month's pass, fills a wallet, and each check-in spends one credit. The price follows the student's category: adults, students and children each have their own discount.
The health log speaks up at the door: anything marked warning or critical raises an alert she acknowledges before the check-in goes through. The same step refuses to check one person in twice for the same session.
New students come in through a four-step intake (details, health, membership, consultation), kept as a draft while she types. Then they land in Google Contacts under a label of their own, so they're in her phone and on WhatsApp the moment they sign up.
// partners
The B2B side serves hotels, riads and companies that book group sessions. Each partner keeps its ICE, the Moroccan company identifier an invoice can't go out without, its contacts, synced to Google Contacts under their own label, and every document exchanged with it.
// quotes, invoices, backorders
A quote turns into an invoice line by line. When only part of it is billed now, confirming the invoice offers a backorder: a new draft for what's left, the way Odoo does it. Each invoice line remembers the quote line it came from, so nothing is billed twice.
Numbers like INV-2026-0013 come from a counter per type and year, locked in the same transaction as the insert, so two invoices never share one. Totals are worked out again on the server; whatever the browser sends is ignored. Once issued, a document can't be edited or deleted, only archived with a reason.
// payments
A payment is spread over the open invoices of the same chain, oldest number first, and each one turns partially paid or paid as it fills. Paying more than is owed is refused, with the balance in the message.
Every payment dialog carries an id of its own, and the database accepts each id once, so a double tap on a slow connection records one payment, not two.
// the paper
Quotes and invoices print to PDF in the browser, nothing to run on a server: French labels, TOTAL HT and TTC, the studio's logo, signature and bank details from its profile. An invoice drawn from a quote says so, "Reliquat du devis", and the amount is written out in words by a small French number speller written for it.
// stack
- next.js
- typescript
- supabase
- drizzle
- shadcn/ui
- tanstack table
- google apis
- pwa