Skip to main content

Command Palette

Search for a command to run...

Building a Production Gym Management System with Ruby on Rails

How a gym replaced paper registers with a Rails app featuring biometric attendance and WhatsApp automation.

Updated
โ€ข7 min read
Building a Production Gym Management System with Ruby on Rails
L
Ruby on Rails engineer with 2 years of experience building production systems โ€” including government management platforms (Ministry of Tourism, India), HR and manufacturing workflow systems, and Spine Fitness, a gym management SaaS I independently built and deployed. Reached the global final round in Empire Flippers' engineering assessment process. Writing about backend architecture, production debugging, integrations, and real-world Rails engineering.

Hey there!

I'm Lakshay, and I built Spine Fitness โ€” a full-stack gym management system using Ruby on Rails that's currently deployed in production and used daily by a real gym in Dwarka, New Delhi.

The gym was managing 200+ members using physical notebooks. Membership renewals were missed, payments were scattered, and attendance tracking was a mess. I built a web platform to replace all of it.

๐Ÿ”— Live at: spine-fitness.com ๐Ÿ’ป Source Code: GitHub Repository


The Problem: Death by Notebooks

Before the software, the gym staff dealt with:

  • Member registrations handwritten in registers

  • Membership renewals forgotten โ€” lost revenue

  • Attendance tracked inconsistently (or not at all)

  • Payment history scattered across different books

  • Inventory (equipment, supplements) completely untracked

  • Member communication required manual phone calls

Result? Human errors, missed renewals, zero visibility, and hours of admin work daily.


The Solution: Spine Fitness

A centralized web platform where gym administrators manage everything digitally โ€” from member onboarding to automated WhatsApp expiry reminders.

Tech Stack at a Glance

Layer Technology
Backend Ruby on Rails 7.1, Ruby 3.1.4
Database MySQL (CleverCloud)
Frontend HTML, CSS, JavaScript (Hotwire/Turbo + Stimulus)
Hosting Render
Biometric Bridge Python 3 (pyzk + requests) โ€” runs on gym laptop
WhatsApp API Interakt
PDF Reports Prawn
Scheduling cron-job.com
Hardware ZK Fingerprint biometric device

โš ๏ธ Update (2026): This system originally used Interakt for WhatsApp messaging. Due to reliability issues, I later replaced it with a direct integration using Meta Cloud API.

๐Ÿ‘‰ Full migration breakdown: How I Replaced Interakt with Meta Cloud API

Architecture Overview

Gym Admin (Browser)
   โ”‚
   โ–ผ
Ruby on Rails Application (Render)
   โ”‚
   โ”œโ”€โ”€ MySQL Database (CleverCloud)
   โ”‚
   โ”œโ”€โ”€ WhatsApp Messaging (Interakt API)
   โ”‚
   โ”œโ”€โ”€ Scheduled Jobs (cron-job.com)
   โ”‚
   โ””โ”€โ”€ REST API: POST /api/biometric_attendances
                    โ–ฒ
                    โ”‚ HTTP POST (every 20 seconds)
                    โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  Python Bridge      โ”‚
         โ”‚  (bridge.py)        โ”‚
         โ”‚  Runs on gym laptop โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    โ”‚ pyzk SDK
                    โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  ZK Fingerprint     โ”‚
         โ”‚  Biometric Device   โ”‚
         โ”‚  (192.168.1.201)    โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The system is multi-language โ€” Ruby on Rails handles the web app and business logic, while a Python script running on the gym's local laptop bridges the physical biometric device to the cloud-hosted Rails API.

Python Biometric Bridge (Runs on Gym Laptop)

The biometric fingerprint device sits on the gym's local network. It doesn't natively talk to cloud APIs. So I wrote a Python bridge script that runs continuously on the gym's Windows laptop:

# biometric_bridge/bridge.py

from zk import ZK
import requests
import time
from config import *

def send_to_rails(payload):
    try:
        response = requests.post(
            RAILS_API_URL,
            json=payload,
            timeout=5
        )
        print(f"Sent: {payload} | Response: {response.status_code}")
    except Exception as e:
        print("Rails API error:", e)

def main():
    zk = ZK(
        DEVICE_IP,
        port=DEVICE_PORT,
        timeout=DEVICE_TIMEOUT,
        password=0,
        force_udp=False,
        ommit_ping=False
    )

    print("Connecting to biometric device...")

    try:
        conn = zk.connect()
        conn.disable_device()

        last_sent = set()

        while True:
            attendances = conn.get_attendance()

            for att in attendances:
                key = f"{att.user_id}-{att.timestamp}"

                if key in last_sent:
                    continue

                payload = {
                    "compcode": COMP_CODE,
                    "user_id": att.user_id,
                    "timestamp": att.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
                    "device_sn": conn.serial_number
                }

                send_to_rails(payload)
                last_sent.add(key)

            time.sleep(POLL_INTERVAL_SECONDS)

    except Exception as e:
        print("Device connection error:", e)
    finally:
        try:
            conn.enable_device()
            conn.disconnect()
        except:
            pass

if __name__ == "__main__":
    main()

How it works:

  1. Connects to the ZK biometric device on the local network (192.168.1.201:4370) using the pyzk library

  2. Polls attendance logs every 20 seconds

  3. Deduplicates locally using an in-memory set (last_sent)

  4. Sends each new punch as an HTTP POST to https://spine-fitness.com/api/biometric_attendances

  5. Runs as a background process via a .bat file on Windows startup

:: biometric_bridge/start_biometric.bat
cd C:\biometric_bridge
python bridge.py

This makes the system truly full-stack โ€” from hardware on the gym floor, through a Python bridge on a local laptop, to a Rails API in the cloud.

๐Ÿ—„๏ธ Database Design: Master-Transaction Pattern

I structured the database into Master tables (static data) and Transaction tables (operational data):

Master Tables:

  • mst_members_lists โ€” Member profiles

  • mst_membership_plans โ€” Plan types (monthly, quarterly, annual)

  • mst_staff_lists / mst_trainer_lists โ€” Staff records

  • mst_stock_lists โ€” Equipment & inventory

Transaction Tables:

  • trn_member_subscriptions โ€” Active subscriptions with dates

  • trn_member_attendances โ€” Biometric punch records

  • trn_payments โ€” Payment records linked to subscriptions

  • trn_whatsapp_logs โ€” Message delivery tracking

  • trn_audit_trials โ€” Full audit log of all actions

This separation keeps queries efficient and business logic clean.


Key Features

1. Member Management

Members get auto-generated codes using a custom GlobalCodeGenerator:

@Lastcode = generate_code(
  table: MstMembersList,
  column: "mmbr_code",
  prefix: "M",
  compcode: session[:loggedUserCompCode]
)

Each member links to subscriptions, payments, and biometric mappings.

2. Subscription & Payment Tracking

The system supports partial payments โ€” a real-world requirement:

def payment_status(subscription)
  paid  = total_paid(subscription)
  final = subscription.ms_final_amount.to_f

  return "PAID"    if paid >= final
  return "PARTIAL" if paid > 0
  "DUE"
end

3. Biometric Attendance

A fingerprint device sends punch data to a REST API endpoint. The system maps biometric IDs to members, deduplicates punches, and validates subscriptions in real-time.

๐Ÿ“ I wrote a full deep-dive on this โ†’ [Link to your biometric blog post]

4. WhatsApp Automation

Members automatically receive WhatsApp messages for expiry reminders through a 3-stage pipeline:

Cron Trigger โ†’ Background Job โ†’ Interakt API โ†’ Webhook Delivery Tracking

# Daily cron hits this endpoint
def send_expiry_whatsapp
  return head :unauthorized unless params[:token] == ENV['CRON_SECRET']

  MembershipExpiryWhatsappJob.perform_later(:expiring)
  MembershipExpiryWhatsappJob.perform_later(:expired)
end

Messages are tracked through their full lifecycle:

QUEUED โ†’ SENT โ†’ DELIVERED โ†’ READ (or FAILED)

5. Admin Dashboard

Single-screen overview: Active/Expiring/Expired counts, today's collections, due payments, inventory status โ€” all preloaded in bulk to avoid N+1 queries:

def preload_members
  member_ids = @latest_subs.map(&:ms_member_id).uniq
  @members_map = MstMembersList
    .where(mmbr_compcode: @compcode, id: member_ids)
    .each_with_object({}) { |m, h| h[m.id.to_s] = m }
end

Challenges & Solutions

Challenge Solution
Biometric device sends raw punch data Built REST API with mapping, deduplication, and subscription validation
WhatsApp duplicate messages Check trn_whatsapp_logs before sending
Dashboard slow with 200+ members Bulk preloading with hash maps
Tracking message delivery Interakt webhook controller for real-time status
Multi-user access control Module-based permission system

Key Learnings

  1. Real business requirements are messy. Partial payments, expired members punching in, duplicate scans โ€” you don't think about these in tutorials.

  2. Background jobs are essential. WhatsApp notifications can't block the request cycle.

  3. Audit logging is non-negotiable. When real money is involved, every action needs a trail.

  4. N+1 queries become real when you have 200+ members on a dashboard.

  5. External APIs need defensive coding. Always handle failures, log responses, and plan for timeouts.


What's Next

  • SMS fallback for members not on WhatsApp

  • Member-facing mobile view for self-service

  • Revenue analytics and trend reports

  • Online payment collection


Final Thoughts

Building Spine Fitness taught me that production software for a real business is fundamentally different from side projects. The requirements come from real workflows, bugs have real consequences, and watching a gym owner ditch their notebooks for your app is incredibly satisfying.

If you're looking to build something meaningful with Rails, start by finding a real problem around you. The best portfolio projects are the ones that actually get used.


๐Ÿ”— Live: spine-fitness.com ๐Ÿ’ป Source: github.com/imlakshay08/spine-fitness-gym-management-system

If you found this helpful, drop a โค๏ธ and follow me for more real-world Rails content!

Building a Production Gym Management System with Ruby on Rails