Just Jasper
All projects
01Case study

Relay
Desk

A WhatsApp assistant for a dance studio. It reads what students and coaches talk about and gets each coach ready for their next class.

It runs as a few small services around one WhatsApp connection. The studio's number stays safe while new features keep getting added.

  • ~180students
  • ~6coaches
  • 33safety rules, each tested
  • 5separate services
T-5 · to the coachTonight's class, 7pm
  • Two students mentioned a sore ankle
  • One is back after three weeks away
  • Group asked to run the new combo again
7 messages · 1 summary

02The project

The problem

Pretty much everything at the studio happens in WhatsApp. Coaches and students talk in program groups and direct messages. Before a class, a coach had to read through all of it to see what’s going on with their students. That takes time, and things get missed.

The studio wanted to streamline that and do more with WhatsApp in general, since that’s where they actually connect with their clients. Whatever we built had to live inside WhatsApp, since a separate app coaches had to remember to open just wasn’t going to work.

The Relay Desk Inbox: a list of student threads with unread counts, one open.
The Inbox. Each direct message shows whether the person is a student, a coach or not on the roster at all.

Why WhatsApp

We didn’t really have a choice here. The community already lives in WhatsApp, so if this lived anywhere else it would’ve been dead on arrival.

The official WhatsApp Business API was the obvious first option, but it could only post in groups of up to eight people. The studio’s classes are bigger than that, so that ruled it out. Instead we use Baileys, an open-source library that connects the way WhatsApp Web does. The studio keeps its own number, and it keeps working normally on the phone.

Baileys is unofficial, though, and numbers that send like a bot get banned. Losing the number would mean losing the studio’s main channel. A lot of the design is about sending the way a careful person would.

  • A new app
  • Email
  • Client portal
  • SMS
  • WhatsApp

How it works now

Ten minutes before a class, Claude puts together a summary of what the coach’s students have been saying in the program group and in direct messages. Staff get a few minutes to edit or hold it. Then it goes to the coach, five minutes before class starts.

After class the coach gets a few questions, and their answers turn into a draft post for the group. They can reply YES to send it, NO to drop it, or CHANGE with notes for a new version. All of it runs on its own, without anyone needing to have the app open.

It saves a coach maybe 10 to 15 minutes a class. What I like more is the structure. Every class gets the same before and after, and it kind of turns into a habit.

T-10

Summary is built

Claude reads the coach's groups and direct messages since the last class and writes a short summary.

Who can step in: Nobody needs to. It runs on its own.

Relay Desk · sample data
One class's prep page. The summary is ready before class. Here staff add a line about a student and hold it, so it won't go to the coach until someone releases it.
Relay Desk · sample data
After class, the coach's replies become a draft post for the group. It waits in the Queue for the coach's YES on WhatsApp. Staff can still decide it here.
Relay Desk · sample data
The instructions for each Claude job are a setting. A save is refused if someone saved a newer version first, and every version stays in the history.

03Architecture

Where it started

It started as a way to send WhatsApp messages from Claude Desktop, and that ran into WhatsApp’s rules pretty quickly. A number can only have one live connection per linked device, and a second one knocks the first offline. Every new Claude session would have opened another connection and kicked the last one off. Incoming messages would also only be captured while someone had Claude open.

So one small background process, a daemon, holds the connection full time, and everything else just asks it to send. No message gets missed, even at 2am, and nothing else in the system can knock the number offline by accident.

Keeping the daemon small

The connection is the one part I couldn’t let break, so the daemon does almost nothing else. It saves messages, checks who’s allowed to be messaged and sends at a safe pace. Features like class prep live somewhere else. We can ship them without touching the piece that keeps the number online.

Whether it came from a staff member, a timer or Claude, every message goes through the same checks before it leaves. Group sends are spread over a minute with random gaps, so a class announcement doesn’t arrive like a burst from a bot.

The daemon can also run against a fake WhatsApp, so most changes could be tested without a phone in hand.

Every send, in orderStaff replyClass timerClaudeRon the roster?Aapproved?Ddaily capFfirst contactsrate limiterWhatsApprefused, no token usedSending a batch over one minutea burstwhat we send
A large batch open in the Queue with a warning about how many recipients have never messaged the studio.
A big batch waiting for approval. Before anyone approves it, the Queue works out how many people have never messaged the studio and how many would go over the first-contact limit.

Two SQLite databases

Everything’s in SQLite, split into two small database files with one owner each. The daemon owns the message log, and the app owns everything else. Staff can read conversations while new messages are still coming in, and backups are just a file copied every night. Moving to the cloud was mostly copying files too.

The daily send limit is counted from the saved messages, so a restart doesn’t reset it. Messages typed on the phone count toward it too. The daemon also keeps its own copy of who’s allowed to be messaged. Even with the rest of the app down, it won’t message someone it shouldn’t.

Daemononly writerMessage logSQLitesent, last 24 hours23/ 50 capcounted from the log,so a restart can't reset itInbox and searchread-onlyAPICRM workerGET onlyApp databaseSQLiterosteraccountsclass prepCRM mirrorone writer per tablenightly copy

The layout

Each outside service, like WhatsApp or the CRM, plugs in through its own small piece in its own process. A problem in one doesn’t take the others down. If the CRM sync fails, messaging keeps working. Each piece also holds only its own password, and the Claude key sits in a process that can’t send anything.

I wanted it modular from the start because I knew new needs would keep turning up, and they did. Testing got easier too, since each piece can be swapped for a fake.

Hover a piece to see what it owns and what it can’t touch.

writeswritesreadsPeoplestudents · coachesDaemonBaileys · one socketAPIcore rulesClaude serviceonly key holderCRM workerGET onlyApp databaseSQLiteMessage logSQLiteWeb appReact · staffRrosterPpace + capsSscan−strip PII✓output checkAapproval

The libraries, and why

I kept the list short, and each one is there for something the studio actually notices.

  • Baileys lets the studio keep its own number and phone. It’s pinned to a version we proved on a real phone, because newer ones failed to connect.
  • SQLite, built into Node, means nothing extra to run or pay for, and backups are just files.
  • The Anthropic SDK with structured outputs, so Claude answers in a fixed shape the app can check. A bad answer gets caught before it reaches anyone.
  • React and Vite for the staff app, which works on a phone and updates live as messages come in.
  • Docker and Caddy to run the whole thing on one small server with HTTPS.

Following one message

writeswritesreadsPeoplestudents · coachesDaemonBaileys · one socketAPIcore rulesClaude serviceonly key holderCRM workerGET onlyApp databaseSQLiteMessage logSQLiteWeb appReact · staffRrosterPpace + capsSscan−strip PII✓output checkAapproval
  1. A student posts in their program's group.

  2. The daemon saves it to the message log right away, so nothing is missed even when no one is looking.

  3. Before class, the API reads the coach's messages from the log and each one is screened for anything trying to trick the AI. Flagged ones are left out of the summary.

  4. Names and numbers are removed, then Claude writes the summary.

  5. The summary is checked for links, numbers and contact details. If anything fails, it isn't sent.

  6. The coach's copy goes out on its own. The group version waits for a person to approve it.

A class's messages with their scan results; one that tries to instruct Claude is marked held.
One class's messages after the scan. A message that tries to give Claude instructions is held, so it never makes it into the summary.

Switching to the CRM

About halfway through, we realized we could sync the studio’s CRM instead of keeping our own roster. Signups already go through it. Now when someone joins a program, they show up in the app on their own and nobody retypes anything. The app still owns the conversations, plus the few things the CRM doesn’t know, like each coach’s WhatsApp number.

The sync runs on its own and can only read from the CRM, so it can’t change the studio’s records. It copies what it reads into the app’s SQLite database, where the rest of the app reads it. If the CRM is slow or down, the app keeps working from the last good copy.

New students are added automatically, but nobody can message them until a person approves the batch. If someone signs up with fake details, they don’t get messaged. A sudden jump in coaches is held until someone confirms it.

writesOur own rostertyped in by handCRM workerGET onlySQLite mirrorin the app databasewritesAPI readsCRM: who is in a classMessaging: what can be sent to themPeoplestudents · coachesDaemonBaileys · one socketAPIcore rulesClaude serviceonly key holderMessage logSQLiteWeb appReact · staff
Needs attention: coaches needing a number, programs with no group and other issues from the sync.
Needs attention lists what the sync couldn't sort out on its own, like a coach with no WhatsApp number yet. Nothing here changes until someone fixes it.

One box instead of two

The first plan kept the WhatsApp connection on a computer at home and ran everything else in the cloud. I dropped that because I wanted it running 24/7 without relying on my home machine. If I had a dedicated machine at home I’d have considered it, but for what we’re doing an EC2 instance made more sense. Now it all runs on one small server on AWS. There’s one place to update and back up, and nothing at home has to stay switched on.

The server itself is locked down with no remote login. Each service only gets the password it needs.

04Security

Why it mattered

We’re talking to the studio’s clients directly. Their numbers and emails are in there, and we’re saving their messages. On top of that, the WhatsApp library isn’t an official one. If we send too much or act like spam, the number can get banned.

So the rules are written down as numbered invariants, 33 of them, and each one has a test that fails if it breaks. Most are checked at the very last step before a message goes out, so they apply no matter where the message came from.

The rules

Tap a rule to see where it’s enforced.

writeswritesreadsPeoplestudents · coachesDaemonBaileys · one socketAPIcore rulesClaude serviceonly key holderCRM workerGET onlyApp databaseSQLiteMessage logSQLiteWeb appReact · staffRrosterPpace + capsSscan−strip PII✓output checkAapproval
Relay Desk · sample data
A batch Claude queued with its tools. Nothing sends until a person approves it. Then it goes out a few seconds apart.
Relay Desk · sample data
Replying from a phone. Claude drafts the reply from the thread. Pressing Send is the approval, and there are a few seconds to cancel.

05Working with Claude

Where I stepped in

My first ask was basically, what would the architecture be for this problem? What runs today is pretty close to what came back. I mostly had to step in on business details Claude just didn’t have the context for, like when a summary should actually land. Security was the other one, where it was sometimes too strict and sometimes not strict enough.

Then there was anything that needed a real phone. Claude can’t reach WhatsApp from where it runs, so I checked every pairing and live send myself.

Examples

Tap a card to see what changed.

06Process

How we worked

The first 20% or so was the scaffold, getting the processes and boundaries in place. After that we went feature by feature, and I’d give the go-ahead before each next step.

CLAUDE.md was the main thing. It had the architecture, the commands and the invariants, and every session started from it. Each area also got its own plan with dated decisions. I could clear sessions a lot and hand off through those files, without relying on memory.

Anything risky ran on a copy of the live data first, and every feature was checked live before it shipped. Later we broke rules on purpose to see whether the tests noticed. The ones that didn’t got fixed.

My thoughts

Relay Desk started as a way to send WhatsApp messages from Claude Desktop and grew into a set of small services running on AWS. Looking back, a few things stuck with me, some about how I work and some more personal.

Professionally

AI can help with every part of a build. On this one it helped most in the middle, with technical hurdles like getting everything running on AWS. I didn’t know a lot of those mechanics going in. As Claude raised them, I had it help me research what each one meant for the studio, and then I made the call. The hardest calls were about security and figuring out which concerns were actually worth the time, and Claude didn’t always get that call right.

Knowing the product is what anchored those calls. Business process is most of what makes software work. I knew how the team would use it and where it was likely to cause friction. I also knew the early decisions weren’t firm, so I kept the architecture modular. When the requirements changed, new features stayed cheap to add. That’s how I’ve built and directed products before AI, and it carried over almost exactly. Claude gets much further when I bring in what it can’t know about the business and the people using it.

Not long ago, a build like this would have cost a small business a lot of money. The studio’s CRM has a similar feature, but it falls a little short of what they need. With Claude I was able to build something that does more than their full-service CRM, in a reasonable amount of time. I think that shows where products and SaaS are heading.

Personally

What surprised me was how much work it is to build on someone else’s platform and follow their rules. Without WhatsApp this would have been a pretty straightforward build. Most of the complexity came from working within its rules, and that’s also where most of the creative problem solving came from.

I enjoyed learning the mechanics and a lot of new libraries along the way. I also spent time studying how other people prompt AI for visuals and how they build their skills. That’s a big part of why this page looks the way it does. What I’m proudest of is that it’s a full A-to-Z system, built mostly with Claude, and the studio actually uses it every day.

Going into the next project, I’d start with the product again. Claude can help with the rest, and it does a lot better when I know what we’re building and who it’s for.