← All posts
AngularNestJSArchitectureSaaS

Building Remanso: notes from a white-label scheduler for therapy

Jun 24, 2026 · 5 min

The first psychologist I built this for scheduled like most of them do: over WhatsApp, from memory, and praying nobody asked to reschedule on a Sunday. The problem wasn’t a missing feature — it was that booking, reminding, and joining the session were three separate things that depended on a human stitching them together by hand every single time. Remanso is what I built to collapse them into one flow, and these are the engineering notes from doing it.

The decision that defines the project: it’s a product, not a site

I could have built one psychologist’s website. Instead I decided, from line one, that this would be a white-label product: a scheduler that rebrands for the next practice by changing configuration, not forking the repo. That decision is what orders everything else.

In practice it means a rule that’s almost dumb in its simplicity: the client’s name does not exist in the code. The practice name, the colors, the contact, the services, the hours — it all lives in configuration and environment variables. If a client name leaks into a template or an email, it isn’t a detail: it’s a bug. Standing up the second practice should be filling in config, not opening a find-and-replace.

Making it configurable after the fact is a rewrite. Baking it into the base from day one is just discipline.

The patient never logs in

The obvious temptation was to give the patient an account. I refused. Nobody is going to create a username and password to ask for a therapy appointment — and forcing them to is the surest way to lose the booking at the exact moment of highest friction.

So the entire patient experience — book, cancel, reschedule, download the appointment to their calendar — runs against a public code per appointment. The code is the credential, with its own narrowly-scoped surface on the server: it lets you touch that one appointment and nothing else. Zero passwords to forget, zero signup screen between the patient and their slot. The backend still doesn’t trust the client; the thing that authorizes just happens to be a single-purpose code instead of a session.

Reusing the Wallet module without contaminating anything

I already had the Google Wallet pass solved from another of my products, and reusing it was obviously the right call — but sharing the same issuer across two products is exactly where things get messy. If Remanso’s pass classes and the other product’s live under the same issuer with no separation, you end up with one product’s pass showing up where the other’s should.

The fix was boring on purpose: a guard prefix on the class IDs that isolates Remanso from anything else under that issuer. Reusing infrastructure is cheap; reusing it without isolation is how you ship a production bug you only catch once it’s the wrong pass on someone’s phone.

Manual WhatsApp and SMTP, on purpose

I could have integrated the WhatsApp Business API and an email provider with templates and webhooks. For a first deployment, it wasn’t worth it. Transactional email goes out over nodemailer on SMTP — confirmations, reschedules and reminders, branded for the practice — and for the moments that genuinely call for a human, a WhatsApp click-to-send button opens the chat with the message pre-filled.

It’s not laziness, it’s focus. The job of an MVP is to validate the flow that creates value — book, remind, make the session — not to accumulate plumbing nobody asked for yet. The heavy integration earns its place when a real client needs it, not before.

SSR for a local business

Remanso is a local-business site people open from their phone, almost always off a search. That makes SSR a product decision, not a technical indulgence: the page has to paint fast and be indexable, not ship a blank shell while a bundle loads. The front is Angular 22 + Tailwind 4 with server-side rendering, and i18n via Transloco because the content is bilingual from the start. The server pays for the first paint; the business reaps the SEO.

The quiet trap: UTC wall-clock time

The bug that most easily slips into an appointment system is the one-hour drift. The patient sees 4:00, the appointment lands at 5:00 on the psychologist’s calendar, and the .ics says something else — because each layer interpreted the date its own way and nobody agreed on a convention.

An appointment passes through too many systems — the booking, Google Calendar, Meet, the .ics, the Wallet pass, the reminder — to improvise the timezone in each one. So wall-clock time is handled with a single UTC convention end to end, so the time the patient sees, the one that lands on the calendar, and the one that travels in the pass are the same time. It’s the kind of thing nobody notices when it’s right and that quietly destroys trust in the product when it’s wrong.

And yes, it’s tested and deployed

None of this lives on my machine. The backend runs on Railway with managed Postgres and Redis; the front on Firebase App Hosting with a custom domain; Prisma migrations run when the container boots. Reminders and email go out of Redis-backed queues (Bull) with configurable offsets, off the request path. I worked with agent assistance to move fast, but the bar didn’t move: strict types, tests where they matter, and deploying on a pipeline rather than by hand.

What I learned

“Product, not site” is a design constraint, not a marketing one. The moment I banned the client’s name from the code, everything else fell into place: config became the source of truth, rebranding became trivial, and the second client stopped being a fork and became a configuration file.

Deliberately simple isn’t technical debt. Manual WhatsApp and SMTP aren’t shortcuts I’m embarrassed by — they’re the right answer to the right question for an MVP. The debt is building the heavy integration before there’s anyone it serves.

Time consistency is invisible engineering. Nobody congratulates you because the appointment lands at the right hour across five different systems. But it’s exactly the kind of thing that, when it fails, makes a patient distrust the whole platform — so you design it, you don’t improvise it.