- name
- sesion-mental-health-clinic-platform
- description
- AI-powered practice management platform for psychologists with scheduling, WhatsApp automation, AFIP billing, video calls, and Claude AI integration
- triggers
- ["set up sesion mental health platform","configure sesion for psychology clinic","integrate claude ai with sesion","configure whatsapp automation for clinic","set up afip electronic invoicing","configure livekit video consultations","deploy sesion workflow orchestration","configure mercadopago billing integration"]
# Sesión Mental Health Clinic Platform
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Sesión is a comprehensive SaaS platform for psychology clinics in Argentina that orchestrates appointment scheduling, WhatsApp automation (via Baileys), AFIP-compliant electronic invoicing, secure video consultations (LiveKit), and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6.
## Architecture Overview
Sesión uses a microservices architecture with:
- **Backend**: NestJS (TypeScript) with Prisma ORM
- **Frontend**: SvelteKit 5 with Tailwind CSS
- **Database**: PostgreSQL with Prisma
- **AI**: Anthropic Claude (Opus 4.6 for deep analysis, Sonnet 4.6 for real-time)
- **Video**: LiveKit WebRTC infrastructure
- **Messaging**: Baileys (WhatsApp Web API)
- **Payments**: Stripe (international), MercadoPago (Argentina)
- **Caching**: Redis for sessions and real-time data
## Installation & Setup
### Prerequisites
```bash
# Required versions
node >= 20.x
postgresql >= 15.x
redis >= 7.x
```
### Clone and Install
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
# Install dependencies for backend
cd backend
npm install
# Install dependencies for frontend
cd ../frontend
npm install
```
### Environment Configuration
Create `.env` files in both backend and frontend directories:
**Backend `.env`:**
```env
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/sesion_db"
# Redis
REDIS_URL="redis://localhost:6379"
# Anthropic Claude AI
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
# WhatsApp (Baileys)
WHATSAPP_SESSION_DIR="./whatsapp-sessions"
WHATSAPP_WEBHOOK_SECRET="${WHATSAPP_WEBHOOK_SECRET}"
# LiveKit Video
LIVEKIT_API_KEY="${LIVEKIT_API_KEY}"
LIVEKIT_API_SECRET="${LIVEKIT_API_SECRET}"
LIVEKIT_WS_URL="wss://your-livekit-server.com"
# AFIP (Argentina Tax Authority)
AFIP_CUIT="${AFIP_CUIT}"
AFIP_CERT_PATH="./certs/afip-cert.pem"
AFIP_KEY_PATH="./certs/afip-key.pem"
AFIP_ENVIRONMENT="testing" # or "production"
# Payment Providers
STRIPE_SECRET_KEY="${STRIPE_SECRET_KEY}"
STRIPE_WEBHOOK_SECRET="${STRIPE_WEBHOOK_SECRET}"
MERCADOPAGO_ACCESS_TOKEN="${MERCADOPAGO_ACCESS_TOKEN}"
MERCADOPAGO_PUBLIC_KEY="${MERCADOPAGO_PUBLIC_KEY}"
# Application
JWT_SECRET="${JWT_SECRET}"
APP_URL="http://localhost:3000"
API_URL="http://localhost:4000"
```
**Frontend `.env`:**
```env
PUBLIC_API_URL="http://localhost:4000"
PUBLIC_LIVEKIT_WS_URL="wss://your-livekit-server.com"
```
### Database Setup
```bash
cd backend
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate deploy
# Seed initial data (optional)
npx prisma db seed
```
### Start Development Servers
```bash
# Terminal 1 - Backend (NestJS)
cd backend
npm run start:dev
# Terminal 2 - Frontend (SvelteKit)
cd frontend
npm run dev
# Terminal 3 - Redis (if not running as service)
redis-server
```
## Core Module Usage
### 1. Appointment Scheduling Module
**Creating an appointment with conflict detection:**
```typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { AppointmentConflictError } from './errors';
@Injectable()
export class AppointmentService {
constructor(private prisma: PrismaService) {}
async createAppointment(data: {
patientId: string;
practitionerId: string;
startTime: Date;
duration: number; // minutes
type: 'presencial' | 'virtual' | 'evaluacion';
}) {
const endTime = new Date(data.startTime.getTime() + data.duration * 60000);
// Check for conflicts
const conflicts = await this.prisma.appointment.findMany({
where: {
practitionerId: data.practitionerId,
OR: [
{
startTime: { lte: data.startTime },
endTime: { gt: data.startTime },
},
{
startTime: { lt: endTime },
endTime: { gte: endTime },
},
],
status: { not: 'cancelled' },
},
});
if (conflicts.length > 0) {
throw new AppointmentConflictError(
'Practitioner has conflicting appointments'
);
}
const appointment = await this.prisma.appointment.create({
data: {
...data,
endTime,
status: 'scheduled',
},
include: {
patient: true,
practitioner: true,
},
});
// Trigger WhatsApp confirmation
await this.whatsappService.sendAppointmentConfirmation(appointment);
return appointment;
}
async suggestAlternativeSlots(
practitionerId: string,
preferredDate: Date,
duration: number
) {
const startOfDay = new Date(preferredDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(preferredDate);
endOfDay.setHours(23, 59, 59, 999);
const bookedSlots = await this.prisma.appointment.findMany({
where: {
practitionerId,
startTime: { gte: startOfDay, lte: endOfDay },
status: { not: 'cancelled' },
},
select: { startTime: true, endTime: true },
orderBy: { startTime: 'asc' },
});
// Get practitioner availability rules
const availability = await this.prisma.practitionerAvailability.findMany({
where: { practitionerId, dayOfWeek: preferredDate.getDay() },
});
const suggestions: Date[] = [];
for (const slot of availability) {
const slotStart = new Date(preferredDate);
slotStart.setHours(slot.startHour, slot.startMinute, 0, 0);
const slotEnd = new Date(preferredDate);
slotEnd.setHours(slot.endHour, slot.endMinute, 0, 0);
let currentTime = slotStart;
while (currentTime.getTime() + duration * 60000 <= slotEnd.getTime()) {
const isFree = !bookedSlots.some(
(booked) =>
currentTime >= booked.startTime && currentTime < booked.endTime
);
if (isFree) {
suggestions.push(new Date(currentTime));
}
currentTime = new Date(currentTime.getTime() + 15 * 60000); // 15-min increments
}
}
return suggestions.slice(0, 5); // Return top 5 suggestions
}
}
```
### 2. WhatsApp Automation (Baileys Integration)
**Setting up WhatsApp session and sending automated reminders:**
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
WAMessage,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import * as cron from 'node-cron';
@Injectable()
export class WhatsAppService implements OnModuleInit {
private socket: ReturnType<typeof makeWASocket>;
async onModuleInit() {
await this.initializeWhatsApp();
this.scheduleReminders();
}
private async initializeWhatsApp() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_DIR || './whatsapp-sessions'
);
this.socket = makeWASocket({
auth: state,
printQRInTerminal: true,
});
this.socket.ev.on('creds.update', saveCreds);
this.socket.ev.on('connection.update', (update) => {
const { connection, lastDisconnect } = update;
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !==
DisconnectReason.loggedOut;
if (shouldReconnect) {
this.initializeWhatsApp();
}
}
});
// Handle incoming messages
this.socket.ev.on('messages.upsert', async (m) => {
await this.handleIncomingMessage(m.messages[0]);
});
}
async sendAppointmentConfirmation(appointment: {
id: string;
patient: { name: string; phone: string };
startTime: Date;
type: string;
}) {
const message = `Hola ${appointment.patient.name},\n\n` +
`Tu sesión ${appointment.type} está confirmada para:\n` +
`📅 ${appointment.startTime.toLocaleDateString('es-AR')}\n` +
`🕒 ${appointment.startTime.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })}\n\n` +
`Por favor responde *CONFIRMAR* para confirmar tu asistencia.\n\n` +
`Sesión - Tu asistente de salud mental`;
await this.socket.sendMessage(
this.formatPhoneNumber(appointment.patient.phone),
{ text: message }
);
}
private scheduleReminders() {
// Run every hour
cron.schedule('0 * * * *', async () => {
const now = new Date();
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const in2Hours = new Date(now.getTime() + 2 * 60 * 60 * 1000);
// 24-hour reminders
const upcoming24h = await this.prisma.appointment.findMany({
where: {
startTime: {
gte: new Date(in24Hours.getTime() - 30 * 60 * 1000), // 30-min window
lte: new Date(in24Hours.getTime() + 30 * 60 * 1000),
},
status: 'scheduled',
reminderSent24h: false,
},
include: { patient: true },
});
for (const apt of upcoming24h) {
await this.sendReminder(apt, '24 horas');
await this.prisma.appointment.update({
where: { id: apt.id },
data: { reminderSent24h: true },
});
}
// 2-hour reminders
const upcoming2h = await this.prisma.appointment.findMany({
where: {
startTime: {
gte: new Date(in2Hours.getTime() - 15 * 60 * 1000),
lte: new Date(in2Hours.getTime() + 15 * 60 * 1000),
},
status: 'scheduled',
reminderSent2h: false,
},
include: { patient: true },
});
for (const apt of upcoming2h) {
await this.sendReminder(apt, '2 horas');
await this.prisma.appointment.update({
where: { id: apt.id },
data: { reminderSent2h: true },
});
}
});
}
private async sendReminder(appointment: any, timeframe: string) {
const message = `🔔 Recordatorio: Tu sesión es en ${timeframe}\n\n` +
`📅 ${appointment.startTime.toLocaleDateString('es-AR')}\n` +
`🕒 ${appointment.startTime.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })}\n\n` +
`Nos vemos pronto!`;
await this.socket.sendMessage(
this.formatPhoneNumber(appointment.patient.phone),
{ text: message }
);
}
private async handleIncomingMessage(message: WAMessage) {
if (!message.message || message.key.fromMe) return;
const text = message.message.conversation ||
message.message.extendedTextMessage?.text || '';
const from = message.key.remoteJid;
// Crisis detection keywords
const crisisKeywords = ['suicidio', 'morir', 'acabar', 'terminar todo', 'crisis'];
if (crisisKeywords.some(keyword => text.toLowerCase().includes(keyword))) {
await this.handleCrisisEscalation(from, text);
return;
}
// Appointment confirmation parsing
if (text.toLowerCase().includes('confirmar')) {
await this.handleAppointmentConfirmation(from);
}
}
private async handleCrisisEscalation(phone: string, message: string) {
// Alert practitioner immediately
const patient = await this.prisma.patient.findUnique({
where: { phone: this.normalizePhoneNumber(phone) },
include: { practitioner: true },
});
if (patient?.practitioner) {
// Send urgent notification to practitioner
await this.socket.sendMessage(
this.formatPhoneNumber(patient.practitioner.phone),
{
text: `🚨 ALERTA DE CRISIS 🚨\n\nPaciente: ${patient.name}\nMensaje: "${message}"\n\nContactar inmediatamente.`,
}
);
}
// Send supportive auto-response
await this.socket.sendMessage(phone, {
Ver no GitHub