Case study
VetMiMi
A booking site with private video calls and easy posting for an art therapist in Sydney.
A client project for Daw Mi, an art therapist who works on her own in Sydney. I turned her personal blog into a site in English and Burmese. Clients can book sessions and meet her on private video calls. She can write one post and share it on her website, Facebook, Instagram and LinkedIn.
- Role
- Lead developer: API, website, admin and servers
- Timeline
- Aug - Oct 2026 · from planning to launch
- Status
- Live on AWS
- Type
- Client project
- API actions, all in one shared contract
- 89
- Go tests, run on a real PostgreSQL database
- 454
- channels from one draft: web, Facebook, Instagram, LinkedIn
- 4
- languages: English and Burmese
- 2
01
The brief
Daw Mi's old site looked like a personal blog. She wanted a professional site in English and Burmese that also saved her admin time:
- Booking sessions without emails back and forth.
- Private online sessions without a public video app.
- Sharing her writing on her site and social media without writing it four times.
She works alone, with tens of appointments a month. So running costs had to stay close to zero. And because this is therapy, the booking form asks for no health details. It never suggests it offers emergency help. Booking links stay private.
Before coding, I wrote the proposal, studied nine similar practices and planned the pages and booking rules with her. A teammate and I then turned a design prototype into a Next.js app.
02
How it fits together
Two code repositories, one rule: the browser only talks to the Next.js site.
- Next.js (a website framework) runs the public site and the admin area. Keeping both in one app means one login and one look.
- A Go API does the main work. One program runs in two modes. One mode answers requests and runs the video calls. The other does background jobs. The website host can't do either, so I needed a server that is always on. Go is light, so it fits a small, cheap server.
- PostgreSQL is the main database. Redis holds the job queue. If Redis loses its data, the jobs are rebuilt from PostgreSQL.
- Resend sends email. S3 (AWS file storage) keeps the images, with smaller copies for the web.
03
One shared contract
All 89 API actions are written in one file, openapi.yaml. The Go server code and the website's TypeScript types are both built from it. So the website and the API share one contract.
If they ever stop matching, the build fails. The idea is simple: if a field is renamed, my build should break, not a visitor's booking.
Security lives in the same file. Each action must say who can use it. If one doesn't, the server won't start.
04
No double bookings
The Go API works out free times from her weekly hours, days off and existing bookings. It always uses Sydney time. Daylight saving has its own tests, so the hour the clocks skip or repeat is handled right.
The last check is in the database. PostgreSQL itself refuses two active bookings that overlap. This holds even when many requests arrive at the same moment. If a time is taken, the client sees up to five other free times.
A few more safety details:
- Pressing submit twice gives the same answer. It does not make a second booking.
- Hidden spam traps catch bots. Forms sent in under three seconds are refused.
- Private booking links are stored only in a scrambled (hashed) form. A leaked database has no working links.
05
Email that never breaks a booking
If the site sent email in the middle of a booking, a slow email service could break it. So each email is saved in the database together with the booking change. A background worker then sends it through Resend. It retries up to five times and picks up emails lost in a crash. It never sends the same email twice.
Reminders check the booking again right before sending. So a moved session never gets a reminder for the old time. A failed email never cancels a booking.
There are 12 kinds of email in two languages, each checked by tests.
06
Private 1:1 video calls
Public video apps made no privacy promise. Paid video services charge by the minute. For one client and one therapist, WebRTC fits well. It sends video straight between the two browsers, with nobody in the middle.
- Joining: the Go API helps the two browsers find each other. You need a pass that lasts five minutes. The room opens 15 minutes before the session and closes an hour after. The server saves none of the messages it passes on.
- Hard networks: some phone and office networks block direct calls. Then a relay server (TURN) carries the video. It can't be used to reach inside the server.
- In the browser: you can test your camera and microphone first. A dropped call reconnects on its own. A banner warns you about a weak line.
Calls are never recorded.
07
Write once, post everywhere
At first I planned a full content system (CMS). Once I saw what she really needed, I built a much smaller publishing portal. She writes a piece once, then shares it.
Each post has one version for each channel. It goes from draft to review, approved, scheduled and published. Only the site admin can approve. Approval checks each channel's rules. For example, Instagram needs 1 to 10 images and at most 2,200 characters.
- Website: visitors keep seeing the last published copy until an edit passes review.
- Facebook, Instagram, LinkedIn: each one is sent as its own background job. If we can't tell whether a post went out, it is marked "unknown". It is never re-sent on its own. Nobody wants a double post.
- Not connected? She can copy the text, post it herself and mark it as done.
AI help: Claude Haiku 4.5 can draft the social posts or a translation. It is told to keep her voice and make nothing up. Drafts are never saved on their own. She always edits and approves them.
08
Security and privacy
- Admin sign-in: passwords are stored safely (argon2id). Admin sign-in supports codes from an authenticator app. After 10 failed tries in 15 minutes, the account locks.
- Social media keys are encrypted in the database.
- Logs never hold names, emails, notes or private links.
- Less data: the data plan lists what we choose not to store. A daily job deletes old records.
09
Testing
- 454 Go tests run on a real PostgreSQL database, not a fake one. Each test set gets a fresh database.
- Race tests check what happens when two things happen at once. For example, two people book the same time and only one wins.
- Every safety check is proven. I removed it, watched a test fail, then put it back.
- The website has 179 unit tests. Nine Playwright tests check accessibility, keyboard use and layout.
10
Running it on AWS
Everything runs on one small AWS EC2 server. I set it up with Terraform, a tool that describes servers in code. The API, worker, website, database, Redis and video relay all share it, each with a memory limit.
- Automatic deploys: when the tests pass on GitHub, GitHub Actions puts the new version on the server. Then it checks a health page. If the new version doesn't come up, it goes back to the last one by itself.
- Backups: every night the database is backed up to a private S3 bucket and kept for 30 days. The server can add backups but can't delete them.
- Alarms: CloudWatch watches the server. A separate budget alarm stays even if the server is removed.
- The bigger setup: I also built a larger setup on ECS Fargate and RDS. These AWS services run the app and database for you. Then I took it down. For tens of bookings a month, one server is enough. It also couldn't cheaply open the ports the video relay needs. It is ready if the practice grows.
11
Where it is now
VetMiMi is live on AWS. All four stages are done: the base, booking and contact, video and the publishing portal.
Still to do: Daw Mi checks the emails, a native speaker checks the Burmese text and I add legal pages. I also want end-to-end tests and a practice restore from a backup.
Some trade-offs are on purpose. I wrote them all down. One server means one place that can fail. Video is one-to-one only. Social media links need reconnecting every 60 to 90 days. All of this fits a one-person practice. Each one has a next step if the practice grows.
Stack
- Go
- chi
- sqlc
- OpenAPI 3.1
- Next.js 16
- TypeScript
- PostgreSQL
- Redis
- asynq
- WebRTC
- coturn
- Resend
- Claude API
- Docker
- Terraform
- AWS EC2
- Amazon ECR
- CloudWatch
- GitHub Actions (OIDC)
- Playwright
Last updated: