In plain terms
FitPulse is the software a gym chain runs its business on: who is a member, what they paid for, which branch they belong to, whether they came in today, and what the trainers are owed.
Several separate gym businesses use the same platform at the same time, each with their own branding, their own web address, and data none of the others can reach. Members walk up to a fingerprint reader the gym already owned and are through the door in a second. The owner opens a dashboard and sees today’s check-ins as they happen. At the front desk, staff photograph a national ID or birth certificate and the form fills itself - with badges showing what the AI suggested and what a human still has to confirm.
Why it matters commercially: the gym replaces a spreadsheet, a card machine, and a lot of arguing about who came in when. Onboarding a member stops being ten minutes of transcription from a laminated card. And because every customer company runs on shared infrastructure, the platform can sign a new gym without buying anything new.
The problem
A gym chain is not one business. It is a head office, several branches, staff who belong to exactly one of them, trainers who float between two, and members who bought a plan at one location and expect it to work at another.
Model that badly and every question the software asks the database needs a “…and only for this branch” clause that someone will eventually forget. Forget it once and a receptionist in Dhanmondi is looking at Gulshan’s revenue - or worse, a rival gym’s. That is not a bug report; that is a lost contract.
Model it well and the separation is structural: impossible to forget, because it is not something anyone has to remember.
FitPulse is a platform for that shape of business, serving a web dashboard, a member phone app, biometric hardware, and now document-driven workflows from one system.
Keeping customers apart, in two dimensions
Between businesses: each gets its own database schema. Every customer company gets its own separate space inside a shared database cluster. The web address a visitor arrives on - including a gym’s own custom domain - decides which space is opened, for that request only. A missing filter cannot leak another company’s data because the other company’s tables are not visible at all.
For the business, this is the difference between “we are careful” and “it cannot happen”. It is also what makes custom-domain branding possible: a gym can put the product on their own web address without anyone building them a separate copy of the system.
Inside a business: branch scoping. Within one company, branch access is enforced deep in the data layer rather than screen by screen. Screens ask for “the members I am allowed to see”; the system decides what that means based on the person’s role and which branch they work at. A curious staff member editing the web address in their browser gets a refusal, not another branch’s payroll.
Permissions then resolve three ways at once: the subscription plan says which features exist, the role says which actions are allowed, and the record says whether it is in reach. Any one of them saying no means no. I wrote about the trade-offs of this approach in Schema-per-Tenant Django.
AI setup and member autofill
Adding AI to a multi-tenant product is not “paste an API key in settings and hope”. Gyms upload photos of government IDs - sensitive, regulated, and easy to get wrong. The platform needed a provider layer operators can trust, tenants can quota, and membership features can call without reimplementing failover every time.
Infrastructure in apps/ai (public schema). Providers (Gemini, DeepSeek, OpenAI-compatible local hosts, Tesseract) register with encrypted credentials, model priorities, and task routes. The router tries keys in order, backs off on rate limits, disables bad keys, and falls back to OCR when a text-only model receives a vision task. Usage is logged per tenant; Redis enforces RPM and daily caps. Platform staff manage the lot from an AI Setup tab - kill switch, image policy, route ordering, tenant overrides - gated by platform.ai_setup. Tenants see quota and status under Settings → AI when their package includes settings.ai.
Member onboarding uses it, safely. The Add Member flow accepts uploads or camera capture for member ID, birth certificate, and emergency-contact NID. Each image is stored as a private MemberDocument, normalized (EXIF stripped, WebP for photos), and passed to member_document_extraction. The response fills name, date of birth, gender, address, and NID fields where confidence allows. Fields the user already typed are never overwritten; low-confidence extractions show a warning. Webcam capture compresses to WebP client-side before upload. The model proposes; RBAC and the membership app decide what gets saved.
That split - task registry, tool registry for later agentic work, bounded agent loop - is the same foundation the next AI feature will plug into without touching provider code.
Talking to the turnstile
Attendance is the feature customers actually judge the product on, and it runs on ZKTeco fingerprint terminals sitting on a gym’s local network - not on the internet, and not reachable from a cloud service.
The bridge is a small program that runs on the gym’s own network, speaks to the device the only way the device understands, and forwards the results upstream. It stores events locally when the internet drops, avoids sending anything twice when the connection returns, and treats the device’s own clock as unreliable. Every record carries both the time the device claimed and the time the server received it - because those disagree more often than you would expect, and payroll depends on knowing which one was used.
That bridge is its own project - ZKTeco LAN Agent - and it exists for a commercial reason: the cheaper terminals customers already own cannot reach a cloud service at all, and telling a prospective client to re-equip every branch before they can start is not a sentence that closes a deal.
The attendance data then feeds a rules engine covering late arrivals, leave, overtime and payroll - rules that differ per branch, per role, and occasionally per employee. The outcome for the customer is that the argument about someone’s hours is settled by a record, not by memory.
One API, three clients
The platform has three consumers with almost nothing in common: a React dashboard, a React Native member app, and a turnstile bridge that cannot be redeployed on demand. Designing for the most constrained client made every write idempotent and every interface additive. The full story is in One API, Three Clients.
Holding 10,000+ daily users without a bigger bill
Growth is only good news if serving it stays affordable. Scaling this platform was mostly about refusing to buy hardware first:
- Connection pooling (PgBouncer). The application was exhausting its database connections long before it ran out of processing power - a queue forming at a door that was needlessly narrow. Pooling widened the door for free.
- Caching (Redis). Dashboard figures that were being recalculated from scratch on every page view are now computed once and reused. Pages that felt slow became instant.
- Fixing the queries before adding capacity. The usual culprits: reports asking the database a thousand small questions instead of one, and missing indexes on exactly the branch-and-date combination every report filters by.
- Live updates instead of constant polling. The check-in feed pushes new arrivals to the screen rather than asking the server every five seconds whether anything has changed - less server load and a more responsive product at the same time.
The full breakdown is in Holding 10k Daily Users Before You Buy a Bigger Server.
Shipping without going offline
New versions start up and prove they are healthy before a single member is sent to them; the old version is retired only afterwards. Nobody sees a maintenance page, and nothing has to happen at 2am. Going back to the previous version is redeploying the previous release - deliberately boring, because rollback should never require thinking.
The result is cultural as much as technical: fixes and features reach customers in days instead of waiting for a safe window.
The result
- 10,000+ people a day rely on it, and growth was absorbed by engineering work rather than a larger infrastructure bill.
- Member signup at the desk is faster and less error-prone when ID images drive autofill instead of manual entry.
- AI is operable - keys, models, routes, and quotas are platform-managed rather than hard-coded per feature.
- No downtime during releases, so improvements reach customers continuously instead of in nervous batches.
- A new gym can be onboarded onto the existing platform with its own branding and its own domain - no separate installation to maintain.
- Existing biometric hardware works, which removed a capital cost from the buying decision.
What it demonstrates
This is the project where “full stack” is literal: the data design, the backend, the permission model, the management dashboard, the member phone app, the hardware integration, the deployment pipeline, and now a production AI layer with privacy and tenancy baked in. Every layer was shaped by the same constraint - a mistake made in one customer’s account must never become a problem in another’s.