Case study
SeansaNote · Dictation-first CRM for therapy practices
A therapist gets about ten minutes between two clients. This product is built to fit inside them: two taps from opening the app to a saved, dictated session note, scheduling that costs the therapist nothing to run, and clinical notes the practice owner cannot read.
A self-directed product I own end to end. Research, design, and the whole codebase: one Expo app shipping to web, iOS and Android, on Supabase with per-field encryption and row-level security. Problems and priorities came from conversations with three therapists before the first line of code; it is now in closed beta with ten. Names, clients and notes shown here are invented; nothing on screen belongs to a real person.


The interface is Serbian, in Latin script. That is a product decision rather than an oversight, and the case study explains it below. Every name, client, note and amount on these screens is invented.
The problem
A therapy session runs about fifty minutes. The next client is in the waiting room. Whatever the therapist does not write down in the gap between them gets written at eleven at night, if at all, and by then the useful detail is gone.
Before I wrote any code I sat down with three therapists at one practice: two who own and run it together, and one who works there. That small group happened to cover both situations the product has to serve: the person standing in a hallway with a phone, and the person at a desk asking who owes money and whether Soba 2 is free at four.
What they told me, roughly in their words:
- Scheduling ran on SMS, and the thread restarted every time someone moved a time.
- When nobody was at the desk to take a booking, nobody took the booking.
- Notes lived in a few different places, and finding a recurring theme across months of them meant remembering where it was.
- One of them already recorded herself on her phone after sessions and typed it up later. Voice memos were, in her words, the best thing she had.
- The two owners cared about a different set of things entirely: who is in which room, who is on the roster, and somewhere to post that Soba 2 is out on Friday.
The fourth point is the one that mattered most. A workaround people already perform by hand, recording yourself and then transcribing yourself, is the strongest signal available in product discovery. Nobody invents a tedious ritual for a problem that does not hurt.
Those three are still the closest to it. The product is now in closed beta with ten therapists, and the three from those first conversations are the ones I go back to when a decision is genuinely contested.
So the product has one core problem, and everything else is subordinate to it:
Make the ten minutes between two clients enough.
Which means most of what a practice does is out of scope: no billing integration, no insurance, no messaging inbox, no supervision workflow, no client-facing portal beyond a single booking link.
One thing that is not out of scope, and is easy to mistake for a localisation task: the interface is Serbian, in Latin script. Apple’s speech recognition has no Serbian locale at all, so a phone cannot do this natively. Serbian is also digraphic, and models return a mix of Cyrillic and Latin, so a note written in mixed script stops being searchable. Normalising to one script is not polish, it is what makes the search box work.
Capture: two taps
The app opens on Danas, today. The next session sits in a banner at the top with a microphone on it. Pressing it opens a sheet and recording has already started; there is no second button to begin.

- Recording is already running. There is no second button to start it. The sheet opens and the timer is already moving.
- The client and the session time come from the appointment, so nothing has to be picked or typed.
- Stop is the last action. The transcript comes back, a note is created against this session, and the sheet closes.
Two taps from the day's list to a saved note: the microphone, then stop.
When the therapist stops, the transcript comes back, a session note is created against that appointment and client, and the text is written into it. The sheet says Zapis je sačuvan and closes. Compared with the voice-memo ritual it replaces (record, find the file that evening, play it back, type, paste it somewhere), the whole middle of the loop is gone.
Two decisions behind that never appear on a screen:
- PhoneRecordingthe therapist speaks in the gapholds no provider key
- Edge functionCall to the STT providerSupabase, EU regionthe key lives only here
- SonioxTranscriptSerbian, processed in the EUaudio deleted once the text is back
- NoteText in the recordencrypted per fieldraw transcript kept beside the edited body
34 min of audio → 45 s of textmeasured; about 2% of the recording's length
Dictation is not a feature here, it is the reason the product exists: therapists were already recording themselves on a phone and typing it up in the evening. Two decisions that never show on a screen hold it up. The provider key sits in an Edge function, so it is not in the app anyone installs, and the audio is deleted the moment the text comes back, because a voice from a therapy session is Article 9 health data and has no reason to outlive its transcription. That same layer later shipped as a product of its own.
Keeping the raw transcript alongside the edited body matters more than it looks. Speech recognition gets Serbian clinical vocabulary wrong sometimes, and a therapist correcting a sentence should not be destroying the only record of what was actually said.
Scheduling: removing the channel, not automating it
The therapists asked for less time lost to SMS. The obvious answer is to automate the messages. I think that answer is wrong: an automated SMS still means a time is being negotiated in a thread, and the thread is the cost.
Before, the thread
- clientAny slot this week?
- therapistTuesday 16:00, or Wednesday 11:00.
- clientTuesday doesn't work.
- therapistWednesday 11:00 then?
- clientYes.
- therapistBooked. See you Wednesday.
- clientSorry, I need to move it…
3 messages written by the therapist, and the loop restarts on every change
After, a booking link shipped
- 1Opens the booking linksees free slots onlyclient
- 2Picks a slotname, phone, emailclient
- 3Sends a confirmation emaildouble opt-insystem
- 4Confirmsappointment created automaticallyclient
- 5Reschedules or cancels alonewhile ≥ 24h outclient
0 messages written by the therapist. The client and the system do all of it
The left column is how therapists described their existing flow, not a measurement. The right column is countable from the code. The decision that does not show on any screen: the channel was not automated, it was removed. An automated SMS would still mean someone negotiating a time in messages; a booking link means nobody negotiates. The cost is real: clients in Serbia live on Viber, so a confirmation arrives by email that some will not open, which is why the therapist can still confirm by hand while waiting.
Each therapist can turn on a public link. A client sees only free slots, picks one, and confirms by email; the appointment is then created without the therapist touching it. Later, the client can move or cancel it themselves while they are still at least 24 hours out. Inside 24 hours the link goes read-only, because a session cancelled twenty minutes before it starts is a conversation, not a form submission.
This has a real cost and I want to name it. Clients in Serbia live on Viber and WhatsApp, not email, and some will not open the confirmation. That is why the therapist can still confirm a pending request by hand while waiting, and why the booking is not considered real until someone confirms it.
Confidentiality, decided against the people who asked
The two owners wanted oversight: the roster, the rooms, the money, and the records. The therapist working for them writes notes about people who told her things in confidence.
I built it so the practice owner cannot read a note body. Not as a setting someone can switch off, but as a rule in the database.
| Role | Team | Rooms | Money | Notice board | Note body |
|---|---|---|---|---|---|
| Owner | manages | manages | whole practice | posts | cannot read |
| Admin | manages | manages | whole practice | posts | cannot read |
| Therapist | sees | manages | whole practice | reads | own notes only |
| Assistant | sees | – | whole practice | reads | cannot read |
| Room guestrents a room | – | – | own balance | – | cannot read |
The last column is the whole point. The practice owner sees who owes money, who works when and in which room, but not what a therapist wrote. This is not a setting anyone can switch off: the rule lives in the database, so a request for someone else's note does not return an empty screen, it returns no row. I decided this against what the two owners asked for; they were paying for oversight, and the person whose confidence was at stake was not in the room when that trade-off gets made.
The owner still sees everything a person running a business needs: who owes what, who is booked where, who is on the roster. What they cannot see is the content of a clinical record they did not write and are not the assigned therapist for. The tables holding client details, note bodies and transcripts store ciphertext; the app reads through decrypting views, so a leaked backup carries nothing readable. Notes can also be locked, after which the database refuses body edits, because a session note can become part of a legal record and at that point it has to stop moving.

- Notes for this client, newest first, visible to the author and the assigned therapist, and to nobody else in the practice.
- A padlock: this note is closed. The database refuses edits to its body, because a session note can become part of a legal record.
- Balance and ledger sit beside the notes rather than in a separate billing area. One client, one page.
Money on the left, clinical record on the right, both scoped by the same rules.
Scoping notes this tightly creates a problem it has to solve as well. One therapist told me that finding a recurring theme across months of notes meant remembering which note it was in. Search runs through a database function that only ever looks inside notes the caller is allowed to read, so the same rule that hides a colleague’s record also shapes what the search box can return.
This is the decision I would defend hardest, and it is the one I made against what the people commissioning the work asked for. The person whose confidence was actually at stake was not the one paying.
Two densities, one codebase
The pair at the top of this page is the same Tuesday in the same practice, seen twice: the phone optimises for one record at speed, the desk for many records at a glance. Neither is a scaled copy of the other, and both ship from the same Expo codebase: React Native on the phone, React Native Web at the desk.
The phone leads with the next session and the microphone, because a therapist opening it in the gap is usually about to write something down. The desk leads with the whole day and what it costs, because the person reading it is answering a different question.
The dashboard was the one screen that broke the product’s own rule, that a hairline separates and a container never does, and it was the screen I rebuilt.
First cut
Four equal-weight panels in one box. None of them says what is next.
Worklist shipped
One column, one rhythm. The count sits in the heading, so the quantity lands before the reading does.
The dashboard was the one screen that broke the product's own rule: a hairline separates, never a container. The rewrite does not make it prettier, it makes it shorter. A therapist in the gap is looking for what is next and what still has no note, now the first two rows rather than four panels competing. Labels are the product's own Serbian:Raspored schedule, Zadaci tasks, Bez zapisasessions still missing a note, Dugovanja outstanding balances.
A worklist with nothing on it should be short, not loud. When there is nothing waiting, Danas says so in one line rather than stacking four empty states.
The owner’s side of the product is the same system at a different density:

- The next session, pinned above everything else. Whatever a therapist opens the app for in the gap, it is usually this.
- A section is a small uppercase strip over ruled rows, never a bordered panel. The hairline does the separating.
- Counts ride in the headings, so how much is waiting registers before any row is read.
The worklist at desk density. Four sections, one column, no panel competing with another.

The system underneath
None of the screens above were drawn one at a time. They come out of a token
set and a small group of primitives: 78 tokens, 42 components, a contrast audit
that fails the build, and one SheetModal that serves twenty different places
across both densities.
That is a case study of its own rather than a section of this one, and it is here — the palette and why its neutral axis is warm under a cool accent, the two typefaces and the one job each has, every state the primitives ship with, and the booking sheet across the three shapes it went through.
Outcome
Three things the design removes outright, each countable rather than estimated:
Booking costs the therapist nothing to run. The old loop was a thread that restarted every time somebody moved a time. Now a client picks a free slot, confirms by email, and reschedules themselves while they are more than a day out, and the therapist writes no messages at all.
A session note takes two taps. Microphone, speak, stop. No client picked, no note named, no save pressed, and no evening spent typing up a voice memo.
Transcription is not a wait. Thirty-four minutes of Serbian audio comes back as text in forty-five seconds.
These are counts and one measurement, not outcomes. The product is in closed beta with ten therapists, but nobody has run a study, nothing has been timed, and no one has been asked to compare their week before and after, so nothing here claims a therapist actually got faster.
What I would do differently: I went straight from the conversations into building at full fidelity, with no exploration round in between. The brief was narrow enough that it mostly worked, but it cost me the dashboard. I built the one screen that contradicted my own structural rule and only saw it once it was in front of me. Half a day of rough layouts would have caught that before a week of code did.
The obvious next step is not a feature. It is measuring, because right now I have evidence for the problem, ten people holding the solution, and nothing connecting the two.
I know which number settles it. The product claims the ten minutes between two clients can be enough, so the measure is the lag between a session ending and its note being saved — the median, and the share written inside half an hour. The research said those notes were getting written at eleven at night. If that lag comes back in minutes, the claim holds. If it comes back in hours, it does not, and I would rather find that out than keep the sentence.
Four more follow from the same database, none of them requiring anyone to be asked: how many completed sessions get a note at all, what share of notes are dictated rather than typed, how long a dictated note runs against a typed one, and how many appointments arrive through the booking link instead of being entered by hand.
What is deliberately not on that list is the content of any note. Counts, lengths and timestamps are metadata; the note body is Article 9 health data and the product’s own rule is that nobody but its author reads it. A measurement that breaks the promise the product is built on is not a measurement worth having.
The product that was asked for
I did not decide to build the dictation as a product. It was asked for: someone testing the CRM is on a psychotherapy training programme, word got around there, and several people wanted the dictation without a CRM attached: no clients to track, no rooms to book, just recordings and no way to turn them into Serbian text. Looking further turned up the same gap among lawyers, translators and journalists.
People asking for one piece of a product while declining the rest is the one signal you cannot manufacture, so that layer shipped separately as Prekucaj.rs. Its own case study is here.



