TherapyCRM

Release notes

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

v0.2.61 ·

Client portal with online booking, prepaid packages, secure messaging, custom reports & notification controls

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