| name | sesion-mental-health-orchestration |
| description | SaaS platform for psychology clinics in Argentina with AI-powered scheduling, WhatsApp automation, AFIP billing, and video consultations |
| triggers | ["how do I set up Sesión for a psychology clinic","integrate WhatsApp automation with Sesión","configure AFIP electronic invoicing in Sesión","implement Claude AI for clinical notes in Sesión","set up video consultations with Sesión","create appointment scheduling with Sesión workflow","configure Sesión for Argentine mental health practice","integrate Mercado Pago payments in Sesión"] |
Sesión Mental Health Orchestration Skill
Skill by ara.so — Devtools Skills collection.
Overview
Sesión is a comprehensive SaaS platform for psychology clinics and independent practitioners in Argentina. It orchestrates appointment scheduling, automated WhatsApp messaging (via Baileys), AFIP-compliant electronic invoicing, secure video consultations (LiveKit), and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6.
Tech Stack:
- Backend: NestJS + TypeScript
- Frontend: SvelteKit 5 + Tailwind CSS
- Database: Prisma ORM (PostgreSQL)
- AI: Anthropic Claude (Opus & Sonnet)
- WhatsApp: Baileys library
- Video: LiveKit
- Payments: Stripe + Mercado Pago
Installation & Setup
Prerequisites
node >= 18.0.0
postgresql >= 14
redis >= 6
Project Setup
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
npm install
cp .env.example .env
Environment Configuration
DATABASE_URL="postgresql://user:password@localhost:5432/sesion"
REDIS_URL="redis://localhost:6379"
ANTHROPIC_API_KEY=your_anthropic_api_key
WHATSAPP_SESSION_PATH="./whatsapp-sessions"
LIVEKIT_API_KEY=your_livekit_api_key
LIVEKIT_API_SECRET=your_livekit_api_secret
LIVEKIT_URL=wss://your-livekit-server.com
AFIP_CUIT=your_clinic_cuit
AFIP_CERT_PATH="./certs/afip-cert.pem"
AFIP_KEY_PATH="./certs/afip-key.key"
STRIPE_SECRET_KEY=your_stripe_secret_key
MERCADOPAGO_ACCESS_TOKEN=your_mercadopago_token
JWT_SECRET=your_jwt_secret
APP_URL=http://localhost:5173
API_URL=http://localhost:3000
Database Migration
npx prisma generate
npx prisma migrate dev
npx prisma db seed
Start Development Servers
npm run start:dev
cd frontend
npm run dev
Core Architecture
Prisma Schema Structure
// prisma/schema.prisma
model Patient {
id String @id @default(cuid())
name String
email String @unique
phone String
whatsappOptIn Boolean @default(true)
createdAt DateTime @default(now())
appointments Appointment[]
invoices Invoice[]
clinicalNotes ClinicalNote[]
}
model Appointment {
id String @id @default(cuid())
patientId String
practitionerId String
startTime DateTime
endTime DateTime
type AppointmentType // PRESENCIAL, VIRTUAL, EVALUACION
status AppointmentStatus // SCHEDULED, CONFIRMED, CANCELLED, COMPLETED
patient Patient @relation(fields: [patientId], references: [id])
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
videoSession VideoSession?
}
model Invoice {
id String @id @default(cuid())
patientId String
appointmentId String
afipReceiptType String // A, B, C, M
amount Decimal
status InvoiceStatus
afipCAE String? // AFIP authorization code
afipCAEExpiry DateTime?
patient Patient @relation(fields: [patientId], references: [id])
}
NestJS Backend Services
Appointment Scheduling Service
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { WhatsAppService } from '../whatsapp/whatsapp.service';
import { AppointmentType, AppointmentStatus } from '@prisma/client';
@Injectable()
export class AppointmentsService {
constructor(
private prisma: PrismaService,
private whatsapp: WhatsAppService,
) {}
async createAppointment(data: {
patientId: string;
practitionerId: string;
startTime: Date;
type: AppointmentType;
}) {
const conflicts = await this.checkConflicts(
data.practitionerId,
data.startTime,
);
if (conflicts.length > 0) {
throw new ();
}
appointment = ...({
: {
...data,
: (data..() + * ),
: .,
},
: {
: ,
},
});
..(appointment);
appointment;
}
() {
endTime = (startTime.() + * );
...({
: {
practitionerId,
: {
: [., .],
},
: [
{
: { : startTime },
: { : startTime },
},
{
: { : endTime },
: { : endTime },
},
],
},
});
}
() {
appointment = ...({
: { : appointmentId },
: { : },
});
reminder24h = (appointment..() - * * );
reminder2h = (appointment..() - * * );
.(appointment, reminder24h);
.(appointment, reminder2h);
}
}
WhatsApp Automation with Baileys
import { Injectable, OnModuleInit } from '@nestjs/common';
import makeWASocket, { DisconnectReason, useMultiFileAuthState } from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class WhatsAppService implements OnModuleInit {
private sock: any;
constructor(private prisma: PrismaService) {}
async onModuleInit() {
await this.connectToWhatsApp();
}
async connectToWhatsApp() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_PATH,
);
this.sock = makeWASocket({
auth: state,
: ,
});
...(, saveCreds);
...(, {
{ connection, lastDisconnect } = update;
(connection === ) {
shouldReconnect =
(lastDisconnect. )?.?. !==
.;
(shouldReconnect) {
.();
}
}
});
...(, ({ messages }) => {
.(messages[]);
});
}
() {
(!appointment..) ;
message = ;
.(appointment.., message);
}
() {
formattedPhone = ;
..(formattedPhone, { text });
}
() {
text = message.?.?.();
phone = message...(, );
crisisKeywords = [, , , ];
(crisisKeywords.( text?.(keyword))) {
.(phone, text);
;
}
(text === || text === ) {
.(phone);
}
}
() {
patient = ...({
: { phone },
: { : { : { : } } },
});
}
(: ): {
date.(, {
: ,
: ,
: ,
});
}
(: ): {
date.(, {
: ,
: ,
});
}
}
AFIP Electronic Invoicing
import { Injectable } from '@nestjs/common';
import { AfipWebService } from '@afipsdk/afip.js';
import { PrismaService } from '../prisma/prisma.service';
import * as fs from 'fs';
@Injectable()
export class AfipService {
private afip: any;
constructor(private prisma: PrismaService) {
this.afip = new AfipWebService({
CUIT: process.env.AFIP_CUIT,
cert: fs.readFileSync(process.env.AFIP_CERT_PATH),
key: fs.readFileSync(process.env.AFIP_KEY_PATH),
production: process.env.NODE_ENV === 'production',
});
}
async generateInvoice(data: {
patientId: ;
appointmentId: ;
amount: ;
receiptType: | | | ;
}) {
patient = ...({
: { : data. },
});
lastInvoice = ...({
: { : data. },
: { : },
});
invoiceNumber = lastInvoice ? lastInvoice. + : ;
caeResponse = ...({
: ,
: ,
: .(data.),
: ,
: ,
: patient. || ,
: invoiceNumber,
: invoiceNumber,
: .( ()),
: data.,
: ,
: data.,
: ,
: ,
: ,
: ,
: ,
});
invoice = ...({
: {
: data.,
: data.,
: data.,
: data.,
: ,
: caeResponse.,
: .(caeResponse.),
},
});
invoice;
}
(: ): {
codes = { : , : , : , : };
codes[];
}
(: ): {
date.().(, ).(, );
}
(: ): {
(
,
);
}
}
Claude AI Integration
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ClaudeService {
private anthropic: Anthropic;
constructor(private prisma: PrismaService) {
this.anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
async generateClinicalSummary(appointmentId: string) {
const appointment = await this.prisma.appointment.findUnique({
where: { id: appointmentId },
include: {
patient: {
include: {
clinicalNotes: {
orderBy: { createdAt: },
: ,
},
},
},
},
});
context = appointment..
.( )
.();
message = ...({
: ,
: ,
: [
{
: ,
: ,
},
],
});
message.[].;
}
() {
message = ...({
: ,
: ,
: [
{
: ,
: ,
},
],
});
message.[].;
}
() {
message = ...({
: ,
: ,
: ,
: [
{
: ,
: patientHistory,
},
],
});
.(message.[].);
}
}
LiveKit Video Service
import { Injectable } from '@nestjs/common';
import { AccessToken } from 'livekit-server-sdk';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class LiveKitService {
constructor(private prisma: PrismaService) {}
async createVideoSession(appointmentId: string, userId: string) {
const appointment = await this.prisma.appointment.findUnique({
where: { id: appointmentId },
include: { patient: true, practitioner: true },
});
if (!appointment) {
throw new Error('Appointment not found');
}
const videoSession = await this.prisma..({
: {
appointmentId,
: ,
: ,
},
});
videoSession;
}
() {
at = (
process..,
process..,
{
: participantName,
},
);
at.({
: ,
: roomName,
: ,
: ,
: isPractitioner,
});
at.();
}
() {
...({
: { : sessionId },
: {
: ,
: (),
},
});
}
}
SvelteKit Frontend Components
Appointment Scheduler Component
<!-- frontend/src/routes/agenda/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { Calendar } from '$lib/components/calendar';
import type { Appointment } from '$lib/types';
let appointments: Appointment[] = $state([]);
let selectedDate: Date = $state(new Date());
let loading = $state(false);
onMount(async () => {
await loadAppointments();
});
async function loadAppointments() {
loading = true;
const response = await fetch('/api/appointments', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
});
appointments = await response.json();
loading = false;
}
async function createAppointment(data: {
patientId: string;
startTime: Date;
type: string;
}) {
const response = await fetch('/api/appointments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(data),
});
if (response.ok) {
await loadAppointments();
} else {
const error = await response.json();
alert(`Error: ${error.message}`);
}
}
</script>
<div class="agenda-container">
<h1>Agenda Inteligente</h1>
<Calendar
appointments={appointments}
selectedDate={selectedDate}
onCreate={createAppointment}
{loading}
/>
</div>
<style>
.agenda-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
</style>
AI Clinical Notes Assistant
<!-- frontend/src/routes/session/[id]/notes/+page.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import { Button } from '$lib/components/ui';
let rawNotes = $state('');
let structuredNotes = $state('');
let generating = $state(false);
async function generateStructuredNotes() {
generating = true;
const response = await fetch('/api/ai/structure-notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
appointmentId: $page.params.id,
rawNotes,
sessionType: 'individual',
}),
});
const data = await response.json();
structuredNotes = data.structuredNotes;
generating = false;
}
async function saveNotes() {
await fetch(`/api/appointments/${$page.params.id}/notes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({ content: structuredNotes }),
});
}
</script>
<div class="notes-editor">
<h2>Notas de Sesión</h2>
<div class="editor-section">
<label for="raw-notes">Notas dictadas:</label>
<textarea
id="raw-notes"
bind:value={rawNotes}
placeholder="Paciente reporta mejoría en ansiedad..."
rows="8"
/>
<Button onclick={generateStructuredNotes} disabled={generating}>
{generating ? 'Generando...' : '✨ Estructurar con IA'}
</Button>
</div>
{#if structuredNotes}
<div class="editor-section">
<label for="structured-notes">Notas estructuradas:</label>
<textarea
id="structured-notes"
bind:value={structuredNotes}
rows="12"
/>
<Button onclick={saveNotes}>Guardar Notas</Button>
</div>
{/if}
</div>
<style>
.notes-editor {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.editor-section {
margin-bottom: 2rem;
}
textarea {
width: 100%;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
font-family: inherit;
}
</style>
API Endpoints
REST API Reference
POST /api/auth/login
POST /api/auth/register
POST /api/auth/refresh
GET /api/appointments
POST /api/appointments
PATCH /api/appointments/:id
DELETE /api/appointments/:id
GET /api/appointments/:id/conflicts
GET /api/patients
POST /api/patients
GET /api/patients/:id
PATCH /api/patients/:id
GET /api/patients/:id/history
POST /api/whatsapp/send
GET /api/whatsapp/messages/:patientId
POST /api/whatsapp/opt-out/:patientId
POST /api/invoices
GET /api/invoices
GET /api/invoices/:id/pdf
POST /api/invoices/:id/send-whatsapp
POST /api/video/sessions
GET /api/video/sessions/:id/token
POST /api/video/sessions/:id/end
POST /api/ai/structure-notes
POST /api/ai/generate-summary
POST /api/ai/detect-risk
API Client Usage
export class SesionAPIClient {
private baseURL: string;
private token: string | null;
constructor(baseURL: string) {
this.baseURL = baseURL;
this.token = localStorage.getItem('token');
}
async request(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
...options.headers,
},
});
if (!response.ok) {
const error = await response.json();
(error.);
}
response.();
}
() {
.(, {
: ,
: .(data),
});
}
() {
params = ();
(filters?.) params.(, filters..());
(filters?.) params.(, filters..());
.();
}
() {
.(, {
: ,
: .({ appointmentId, rawNotes }),
});
}
() {
.(, {
: ,
: .({ appointmentId }),
});
}
() {
.(, {
: ,
: .({ participantName }),
});
}
}
Common Patterns
Multi-Tenant Architecture
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentTenant = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.tenant;
},
);
@Get('appointments')
async getAppointments(@CurrentTenant() tenant: Tenant) {
return this.appointmentsService.findAll(tenant.id);
}
Event-Driven Workflows
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AppointmentCreatedEvent } from './events/appointment-created.event';
@Injectable()
export class AppointmentCreatedListener {
constructor(
private whatsapp: WhatsAppService,
private calendar: CalendarService,
) {}
@OnEvent('appointment.created')
async handleAppointmentCreated(event: AppointmentCreatedEvent) {
await this.whatsapp.sendAppointmentConfirmation(event.appointment);
await this.calendar.syncAppointment(event.appointment);
await this.scheduleReminders(event.appointment);
}
}
Caching Strategy
import { Injectable } from '@nestjs/common';
import { Cache } from '@nestjs/cache-manager';
@Injectable()
export class PatientsService {
constructor(
private prisma: PrismaService,
private cacheManager: Cache,
) {}
async getPatient(id: string) {
const cacheKey = `patient:${id}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) return cached;
const patient = await