I Built a Full Gym Management Platform Because the Check-In Was a Clipboard

Get the project:
- BJJChat - Full-stack BJJ gym management platform
- All my tools - Agents, skills, and plugins
I walked into the gym and signed my name on a clipboard. A literal clipboard. Paper, pen, columns for date and name. This was 2025.
I'd just come from building distributed systems that manage Bitcoin ATM fleets across 67 servers. I was looking at a clipboard.
That night I started writing code.
The clipboard problem
Every gym management platform I evaluated had the same disease. They were built by software people who don't train. The workflows made sense on a whiteboard but fell apart on the mat. Check-in required pulling out your phone, opening an app, finding the right class, tapping a button. By the time you're done, warmups started without you.
The other option was dedicated kiosk hardware. Expensive boxes with proprietary software that lock you into monthly contracts. One vendor wanted $200/month for a tablet that scans QR codes. A tablet.
I wanted something different. Walk up to a screen, it sees your face, you're checked in. No phone. No QR code. No clipboard. Just show up and train.
Face recognition on a $300 tablet
The kiosk runs as a dedicated Next.js app on any tablet with a camera. When a member walks up, face-api.js detects their face client-side and generates a 128-dimensional descriptor vector. That vector gets compared against enrolled members using Euclidean distance with a 0.45 threshold.
// Face matching - all client-side, no images stored
const detectFace = async (video: HTMLVideoElement) => {
const detection = await faceapi
.detectSingleFace(video, new faceapi.TinyFaceDetectorOptions())
.withFaceLandmarks()
.withFaceDescriptor();
if (!detection) return null;
// Compare against enrolled members
const matches = enrolledFaces.map(member => ({
memberId: member.id,
distance: faceapi.euclideanDistance(
detection.descriptor,
new Float32Array(member.faceDescriptor)
)
}));
const best = matches.sort((a, b) => a.distance - b.distance)[0];
return best.distance < 0.45 ? best : null;
};
Privacy was non-negotiable. The system never stores actual face images. Only the 128-float descriptor vectors, encoded as base64 strings in a VARCHAR column. You can't reconstruct a face from a descriptor. It's a one-way transformation.
Enrollment takes 3 to 5 sample images with a 70% confidence threshold. Every enrollment and deletion gets logged in an audit trail. If a member wants their biometric data removed, one API call wipes it.
The check-in success screen was the part that surprised me. I added rotating motivational stats: your training streak, total classes this year, classes this week. A subtle chime plays. The screen auto-dismisses after 4 seconds. Members started lingering at the kiosk to see their stats. That wasn't planned.

An AI coach that knows your weak spots
The drill plan generator is the feature I use most. It pulls your training data (technique ratings, recent sessions, neglected positions) and feeds it to Claude to build a personalized 30-minute drill plan.
// AI drill plan generation
const buildDrillPrompt = (userData: TrainingData) => {
const weakTechniques = userData.ratings
.filter(r => r.score <= 2)
.map(r => r.technique.name);
const neglected = userData.techniques
.filter(t => daysSince(t.lastTrained) > 30)
.map(t => t.name);
return `Generate a 30-minute drill plan for a ${userData.belt} belt.
Weak areas: ${weakTechniques.join(', ')}
Not trained in 30+ days: ${neglected.join(', ')}
Training frequency: ${userData.sessionsPerWeek}x/week
Output JSON: { warmup, main_drills, cooldown }`;
};
The output is structured JSON with warmup, main drills, and cooldown. Each drill includes duration, description, focus techniques, rep counts, tips, and even YouTube search terms for reference videos. It streams in real-time so you see the plan building as Claude generates it.
I rated my half guard as a 2 out of 5. The AI now puts half guard sweeps into every single drill plan. It's relentless. And it's right.
36 models and what they track
The database schema grew organically from what I actually needed on the mat. It started with User, Gym, and ClassParticipant. It ended at 36 models across 8,806 lines of Sequelize definitions.
The technique system alone has five interconnected models. Technique holds the basics. TechniqueDetail breaks down step-by-step instructions. TechniqueVideo links YouTube references. TechniqueFlow builds position chains (closed guard to sweep to mount to submission). TechniqueFlowBranch adds conditional logic: if your opponent posts their hand, take the kimura; if they stay tight, switch to an armbar.
The progression system tracks belt requirements per gym. Every academy has different promotion criteria. Some require minimum class attendance. Some require competition experience. Some track specific technique competency. The promotion_rules field is a JSON column on the Gym model, letting each academy define their own curriculum.
// Gym-specific promotion rules
const promotionRules = {
blue_belt: {
min_classes: 120,
min_months: 12,
required_techniques: [
"closed_guard_sweeps",
"mount_escapes",
"basic_submissions"
],
competition_required: false
}
};
There's also an IBJJF integration that scrapes tournament data and athlete rankings with Puppeteer. It's not pretty code. But it works.
11 microsites, 1 backend
The architecture decision I'm most conflicted about is the microsite approach. Instead of one monolithic Next.js app, BJJChat runs 11 specialized sites behind Nginx.
bjjattendance.com is the kiosk. jiujitsutools.com has calculators (belt progression, gi size, weight class, competition timers). bjjconvo.com is the Q&A forum. bjjmatch.com matches training partners. bjjanalytics.com tracks training statistics. Each runs on its own port, each has its own domain.
The upside: each site is focused, fast, and independently deployable. The SEO story is clean. jiujitsutools.com ranks for exactly what it sounds like.
The downside: 13 Docker containers (Nginx + backend + 11 sites) for what could be route groups in a single app. Deployment is a GitHub Actions workflow that SSH's into the server, pulls images from GitHub Container Registry, and restarts containers. It works. But it's a lot of moving parts for one developer.

Coach Iron Grips
Every BJJChat image features anthropomorphic animals in a comic book art style. Bold ink outlines, cel-shaded flat colors, blue mat backgrounds. The mascot is Coach Iron Grips, a turtle wearing round wire-frame glasses and a coral belt with red and white stripes.
Why a turtle? Turtles are patient. They carry their defense with them. They're not fast, but they're persistent. That felt right for jiu-jitsu.
The art style was deliberate. Every BJJ platform uses the same stock photography: intense close-ups of people grappling, dark gym interiors, dramatic lighting. It all blurs together. Comic book animals stand out in a feed. People remember the turtle.
The tools nobody asked for
The dashboard has 20+ training tools that I built because I wanted them, not because anyone requested them.
Takedown Roulette picks a random takedown for you to drill. Position Escape Guide shows your options from any bad position. Bracket Strategy helps you plan a tournament game plan based on your bracket draw. There's a BJJ glossary with every Portuguese term you'll hear on the mat.
The one I use before every competition: the Weight Class Calculator. Enter your current weight, your target division, and the weigh-in date. It tells you if you're going to make weight, what pace you need to cut, and whether you should just move up a division. That last part is important. It saved me from a miserable weight cut once.
What 515 commits taught me
I've been writing software for a long time. But BJJChat was different because I'm the user. I train at a gym that uses this platform. When the kiosk is slow, I feel it. When the drill plan suggests a technique I already drilled yesterday, I notice. When the check-in stats show the wrong streak count, my training partners tell me before I find the bug.
Building for a community you're part of changes the feedback loop. You don't need user research. You don't need surveys. You walk into the gym and the product tells you what's broken.
The platform serves real gyms now. Real members check in with their faces. Real coaches track attendance and plan curriculum. Real competitors use the tools to prepare. None of it started with a product roadmap. It started with a clipboard.

The best software comes from scratching your own itch. The second best comes from scratching your training partner's itch and hoping they don't counter.
One reaction per emoji per post.
// newsletter
Get dispatches from the edge
Field notes on AI systems, autonomous tooling, and what breaks when it all gets real.
You will be redirected to Substack to confirm your subscription.