New features, improvements and fixes in each TherapyCRM release.
v0.2.67 ·
Client email & SMS notifications now send and are fully logged end-to-end
This release makes client-facing notifications actually reach clients and their guardians, and records every send for auditing. Appointment confirmations, cancellations, reschedules, no-shows, and 24h/1h reminders now send email (always) and SMS (when the client has consented), to both the client and their guardians. Outgoing SMS is now logged just like email, with delivery status tracked via provider webhooks. SMS sending is gated by a new per-client consent switch, and message content stays PHI-minimal with an opt-out line.
New features
Client + guardian notifications: appointment confirmation, cancellation, reschedule, no-show, and 24h/1h reminders now send to the client and all linked guardians
SMS delivery log: every outbound SMS is recorded with its status (queued, sent, delivered, failed) and Twilio delivery updates
Per-client SMS consent: a new consent switch on the client record governs all outbound SMS (TCPA/HIPAA), editable from the client edit form
Appointment reminder jobs: 24-hour and 1-hour reminders now dispatch automatically with duplicate-send protection
Invoice notifications: sending an invoice now notifies the client and guardians by email (and SMS when consented)
Improvements
All client notification emails are now recorded in the email log, including portal booking, secure message, document share, and parent-update notices
Notification sends are non-blocking: a delivery hiccup never interrupts creating an appointment or sending an invoice
SMS messages are intentionally PHI-minimal (date, time, practice only) and always include a STOP opt-out
Bug fixes
Fixed appointment notifications never sending: the live appointment path had no notification step, so confirmations/cancellations/reschedules went out to nobody
Fixed outbound SMS never being recorded — there was previously no SMS log at all
Fixed provider delivery webhooks being blocked by CSRF, which prevented email delivery status from updating
Breaking changes
No breaking changes. New notifications are additive and SMS is off until a client consents; existing workflows and data are unaffected
A major feature release that opens TherapyCRM up to families. Parents and guardians can now sign in to a dedicated, invite-only portal to request appointments online and message your team securely. Practices gain prepaid service packages, a custom report builder with saved reports, a personal calendar subscription feed, AI-assisted session-note drafting, and a full notification settings page. Email and SMS notifications are off by default and fully opt-in, while essential account emails (such as password reset) always send. Telehealth video is intentionally not part of this release.
New features
Client/guardian portal: a separate, invite-only sign-in area for parents and guardians, kept isolated from staff accounts so families only ever see the clients they are authorised for
Online booking: guardians can request appointments from the portal based on real staff availability (derived from staff schedules minus leaves, time off, and existing appointments), and staff approve or decline each request
Prepaid service packages: sell bundles of sessions, redeem them against appointments, and track remaining balances per client
Secure messaging: two-way conversations between staff and guardians, scoped to the clients a guardian can access
Custom report builder: build and run reports across your practice data and save report configurations to re-run later
Calendar feed: each staff member gets a personal iCal subscription link to see their TherapyCRM schedule in Google/Apple/Outlook calendars
AI Scribe: draft session notes with AI assistance to speed up documentation
Notification settings: a dedicated Settings page to turn email and SMS notifications on per channel and per event for your practice
Improvements
All email and SMS notifications are off by default and fully opt-in — each practice turns on only the channels and events it wants, while the in-app notification bell always works
Essential account emails (password reset, sign-in verification) always send and are not affected by the notification settings
Outbound booking and message notifications are PHI-safe: they only prompt the recipient to sign in and never include protected health information
Online booking prevents double-bookings: approving a request is rejected if the chosen slot is no longer free
Staff see new booking requests and incoming guardian messages in the in-app notification bell in real time
Bug fixes
This is a feature release; the new portal, booking, messaging, packages, reports, calendar feed, and notification flows ship with their development fixes already included (for example, the booking approval conflict guard and corrected secure-message threading)
Breaking changes
No breaking changes. All capabilities in this release are additive; existing staff workflows, APIs, and data are unaffected
v0.2.56 ·
Accurate client and lead counts everywhere, and the AI agent is now practice-owner-only with branch-aware context
This release fixes a family of data-scoping defects that were quietly making dashboard widgets and branch-filtered views show the wrong numbers, and tightens the new AI agent. The dashboard "Active Clients" KPI was counting leads and prospects as active clients — on the largest production practice that meant showing 174 when only 48 truly-active clients existed. Branch-filtered lead lists were silently dropping every lead whose branchId was NULL — for one practice that was 28 leads disappearing per branch view. The AI agent now uses the practice's configured lead statuses, reports true COUNT(*) totals instead of the truncated page size, and ships restricted to the practice owner role with a fresh PHI-safe practice-context block injected every turn so it can correctly name the branch the user is viewing without ever filtering data by it.
New features
AI agent panel is now restricted to users with the practice_owner role: the Ask agent trigger is hidden in the header for non-owners and a requirePracticeOwner middleware on /api/ai/* returns 403 for any non-owner request
AI agent gets a PHI-safe practice context block every chat turn that lists each branch with its id and name, marks which branch the user is currently viewing in the UI, names the canonical active-client definition, and lists the practice's configured lead-status codes so counting answers stay grounded
AI agent now answers branch questions correctly: tools stay practice-wide so the user can ask cross-branch questions, and the agent names the branch the user is viewing (e.g. 'you are currently viewing Guided Spectrum - Oakville') when scope is relevant
Improvements
list_clients and list_leads tools now return a true total alongside a page sample, so the agent can answer how many active clients or how many leads accurately even when the practice has more rows than the page limit
Shared server/services/lead-statuses.service is now the single source of truth for the practice's lead-status codes — leads list, pipeline, analytics, detail endpoints, and the agent's list_leads tool all read from the same place
AI agent system prompt now explicitly defers to the injected practice context for status semantics rather than inventing its own definition of active, and instructs the agent to declare practice-wide scope when the user asks branch-specific questions
Bug fixes
Fixed the dashboard Active Clients KPI card over-counting by including leads, prospects, and discharged rows in the isActive=true total: simple-storage.getDashboardStats now applies the canonical isActive=true AND status=active AND NOT lead/prospect filter that the clients/stats repository uses; verified against live data the largest practice was showing 174 active clients when only 48 truly-active clients existed
Fixed branch-filtered lead views silently dropping every lead with branchId=NULL: server/routes/leads.ts now applies the canonical branchId=X OR branchId IS NULL filter on the list, pipeline, analytics, and CSV export queries; one practice was losing 28 leads per branch view in production
Fixed AI agent reporting truncated counts (typically capped at 20 or 50) for how many clients questions: list_clients and list_leads tools previously returned rows.length where rows was capped by the page limit; the tools now run a separate COUNT(*) and return a true total alongside the page sample
Fixed AI agent using hardcoded lead_* and prospect_* LIKE patterns instead of the practice's configured codes: the list_leads tool now imports getLeadStatusCodes from the shared lead-statuses service, so custom-configured statuses like lead_no_response and lead_meeting_completed are recognized correctly
Fixed scheduling readiness service treating soft-deleted status=active rows as schedulable: scheduling-readiness.service now applies the full canonical filter (isActive=true AND status=active AND NOT lead/prospect) for the PRACTICE_NO_CLIENTS check
Fixed schedule creation validation counting leads and prospects as active clients: routes/scheduling.ts now applies the canonical filter for the active-clients prerequisite so a practice with only leads cannot accidentally pass readiness
Fixed simple-storage.getClientsByPractice returning soft-deleted rows to the recent-activity feed, /api/clients list, and mobile API list: the helper now excludes isActive=false rows by default since every caller wants live rows
Cleaned up duplicate but unreachable lead handlers in server/routes.ts that hardcoded 6 lead statuses and missed prospects entirely; they were already shadowed by the canonical handlers in server/routes/leads.ts but are now rewritten defensively in case the sub-router is ever disabled
v0.2.55 ·
AI agent can now drive the whole workspace via tool-calling, plus a Linear/Attio UI refresh
Two large product changes ship together. The UI gets a refined Linear/Attio aesthetic refresh — cobalt accent, Geist fonts, dark mode toggle, a tightened sidebar/header, an h-9 primitive set across Button/Input/Card/Badge, denser tables, and a ⌘K command palette. On top of that shell, the side-rail AI agent is rebuilt around a tool-calling architecture: the model now picks from a registry of typed tools to read records (leads, clients, appointments, staff, treatment plans) inline, and proposes writes (add note, set follow-up date, change lead status) that surface as approval cards the user has to explicitly click before any database change happens. The default LLM moved to openai/gpt-4o-mini via OpenRouter for far more reliable tool-calling on the same price tier. PHI stays masked end-to-end — the model only ever sees pseudonyms like "Lead #249" and "Client #123", and write tools re-check practice ownership before mutating.
New features
Side-rail AI agent with tool-calling: reads (get/list leads, clients, appointments, staff, treatment plans, with date / staff / client / search / staleness filters) execute inline; writes (add a timestamped note to a lead, set or clear a follow-up date, change a lead lifecycle status) propose an approval card the user must click before any DB write happens
AI suggestion strip on lead detail pages auto-loads 1-3 contextual next-step suggestions with priority bars, action buttons, and a refresh icon to regenerate; suggestions are PHI-masked and audit-logged
Linear/Attio UI refresh: design tokens, cobalt primary accent (#0049DA), Geist + Geist Mono fonts, full dark mode with a header toggle, refined sidebar, h-9 primitive sizing on Button/Input/Card/Badge, tighter table density
⌘K command palette for navigation and quick create actions (lead, client, appointment)
Multi-turn conversations remember context across turns — the agent resolves pronouns (e.g. 'the lower-scored one') against the running conversation and can chain read → reason → write proposals
Hard guardrails in the agent: cannot convert a lead to an active client (must go through the human-validated conversion wizard); cannot mutate a non-lead record by id; cannot reach across practices even if the model hallucinates an id
Improvements
Default LLM switched from google/gemini-2.5-flash-lite to openai/gpt-4o-mini — same price tier, rock-solid tool-calling on OpenRouter, no MALFORMED_FUNCTION_CALL or tool-namespace quirks
Approval card flow now uses an atomic claim (UPDATE ... WHERE status='pending' RETURNING) so concurrent double-clicks on Approve cannot duplicate an append-only note or audit row — the loser cleanly gets 409 ALREADY_DECIDED
Tool registry decouples the agent surface from the executor: adding a new tool means dropping a file with a Zod schema + an execute function, and it shows up in the agent the next request without a rebuild
Agent loop tolerates Gemini-class model quirks for future provider swaps: strips 'default_api.' namespace prefixes on tool names, treats finish_reason='stop' as terminal only when no tool calls were emitted, and emits OpenAPI-style nullable:true rather than JSON-Schema 2020-12 union types
Every AI interaction (chat, suggestion, tool_call_pending, action_execute) writes a row to ai_interactions with prompt/response previews, tokens used, and a status transition trail for HIPAA audit
Tool-call activity chips render inline in the chat (Looking up Lead #249 → result chip) so the user can see what the agent did rather than just the final answer
Bug fixes
Fixed appointment tool results leaking patient names: the scheduling engine builds appointment.title from the client's full name, so list_appointments and get_appointment were inadvertently passing those titles to the LLM despite the pseudonymous clientLabel; title is now stripped from both tool projections
Fixed a double-execution race on Approve where a quick double-click or client retry could let both requests pass the status===pending check before either ran the update, duplicating notes and audit rows; replaced with a single conditional update that atomically claims the row first
Fixed migration 112 having a ROLLBACK inside a DO block with an EXCEPTION handler — Postgres forbids transaction control in a subtransaction, so the validation block would crash the migration on databases where practice id 1 and user id 1 existed; replaced with a side-effect-free pg_get_constraintdef inspection
Fixed an INVALID_BODY error on the second-and-later chat turn after a write proposal: the prior assistant message had empty string content because the model emitted only tool_calls, which the route schema rejected; the composer now synthesizes a one-line summary so the LLM still has conversation context
Fixed the lead-only guard on agent writes rejecting valid leads in production data that had historical statuses (e.g. lead_no_response) not in the canonical enum; the guard now uses the same startsWith('lead_') || startsWith('prospect_') heuristic the rest of the app uses
Fixed wrong /leads/intake and /clients/intake routes in the ⌘K command palette's Create actions — they fell through to the /:id detail route; updated to the registered /leads/new and /clients/new paths
Fixed AI suggestions component crashing when the API returned an empty suggestions array; component now renders nothing in that case instead of throwing
v0.2.54 ·
Lead conversion now runs through the client edit wizard
Replaces the one-click Convert to Client flow on the lead detail page. The button now opens the client edit wizard with the lead pre-populated, so all required client fields (date of birth, gender, phone, address, state/province, country) must be filled in before the lead can become an active client. The lead row is only flipped to active after a successful save, and the welcome email + initial intake appointment side effects only fire after that successful save. Leaving the wizard without saving leaves the lead untouched. This prevents low-quality client records (missing phone, placeholder DOBs, etc.) from being created during conversion.
New features
Convert to Client on the lead detail page now opens the client edit wizard in convert mode at /clients/:id/edit?convertFromLead=1, with a non-technical banner explaining that nothing changes until the user clicks Save & Convert to Client on the last step
When the contact on a lead is not the client (a parent or guardian leading an inquiry for a child), the wizard pre-seeds the prospect's name, date of birth, and gender on the Personal tab and pre-fills a primary guardian on the Guardians tab from the contact's name, phone, email, and address — only when real values exist (no TBD placeholder fallback)
Improvements
PATCH /api/clients/:id now detects a status transition from a lead status to active and runs the welcome email and initial intake appointment side effects in the same flow, stamping conversionDate server-side so the conversion is atomic and only happens after the client schema fully validates
Welcome email and intake appointment creation are best-effort: a failure in one does not block the other and neither blocks the save, so a missing template or therapy type produces a successful conversion with the configurable side effect skipped instead of an error toast
Save & Convert to Client button stays disabled until every required client field passes validation, with the existing Review-tab error list surfacing each missing field by name (e.g., Phone: Phone number is required) so the converting user knows what to fix
Bug fixes
Fixed low-quality client records being created on conversion: the legacy one-shot endpoint flipped the lead to active without re-running client-side validation, which let leads with no phone, no date of birth, or no address become clients with placeholder or empty values
Fixed Save & Convert showing a confusing Status is required error against a lead's current status code by forcing the wizard's status field to active up front in convert mode, matching the value the PATCH will actually send
Fixed the Convert button being a destructive one-click action with no opportunity to review or fill in missing information before the lead became a client
Breaking changes
POST /api/leads/:id/convert now returns HTTP 410 Gone with error code ENDPOINT_DEPRECATED. Any external integration, mobile client, or script that calls this endpoint directly must be updated to drive the conversion through the client edit wizard (PATCH /api/clients/:id with status=active) so the same required-field validation runs
v0.2.53 ·
Client Edit Persistence Fix and App Engine Runtime Upgrade
Restores reliable saving on the client edit page so date of birth, phone, address, guardian, and emergency contact updates persist correctly and are visible immediately on re-open, eliminates a server-side bug that duplicated guardians on every save, hardens the client PATCH endpoint against cross-client id tampering, and upgrades the App Engine deployment runtime from the now end-of-support Node.js 20 to Node.js 22 so production and preview deployments continue to build.
New features
Client edit Review tab now shows a visible validation banner listing every blocking issue when the Save button is disabled, so users no longer get stuck on a silent form-level error
Auth login route is now reachable at both /auth and /login so the link works regardless of which URL users or scripts navigate to
Improvements
Client edit form reset now runs once per client load and waits for the country-dependent state list to populate, so the State field displays the saved value instead of appearing empty after a country switch
State select control remounts when the country changes so the dropdown reflects the refreshed option list immediately
Server PATCH /api/clients/:id now performs a true upsert on guardians and emergency contacts: existing rows are updated by id, new rows inserted, and rows missing from the payload soft-deleted, replacing the legacy delete-and-reinsert flow that produced duplicates
getClientGuardians query now filters on is_active = true so soft-deleted guardians no longer reappear on the client detail page after a save
Client PATCH no longer wipes guardian or emergency contact relationships when those arrays are omitted from a partial update payload
Bug fixes
Fixed client edit saves silently failing for date of birth, phone, address, and contact fields because the form was being reset on every render, wiping user input before submit
Fixed guardians being duplicated on the client detail page after each save caused by the server deleting and reinserting rows instead of updating in place
Fixed the State select on the client edit page rendering blank when the saved state arrived before the country-dependent state list finished loading
Fixed the /login URL returning a 404 by aliasing it to the existing /auth page