- name
- sesion-mental-health-saas
- description
- Build and customize the Sesión mental health practice management platform with appointment scheduling, WhatsApp automation, AFIP billing, and Claude AI integration
- triggers
- ["how do I set up the Sesión mental health platform","integrate WhatsApp automation with Sesión","configure AFIP electronic billing in Sesión","set up Claude AI models in Sesión","build video consultation features in Sesión","customize appointment scheduling in Sesión","deploy Sesión for Argentine psychology clinics","integrate MercadoPago payments in Sesión"]
# Sesión Mental Health Practice Management Platform
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Sesión is a comprehensive SaaS platform for psychology clinics and independent practitioners in Argentina. It orchestrates appointment scheduling, automated WhatsApp messaging, AFIP-compliant billing, secure video consultations, and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6 models.
## Architecture Overview
Sesión uses a microservices architecture built with:
- **Backend**: NestJS (TypeScript) for API orchestration
- **Frontend**: SvelteKit 5 with Tailwind CSS
- **Database**: PostgreSQL with Prisma ORM
- **Messaging**: Baileys for WhatsApp automation
- **Video**: LiveKit for WebRTC consultations
- **AI**: Anthropic Claude (Opus 4.6 + Sonnet 4.6)
- **Payments**: Stripe + MercadoPago
- **Cache**: Redis for session management
## Installation
### Prerequisites
```bash
# Required versions
node >= 18.0.0
postgresql >= 14.0
redis >= 6.0
```
### Clone and Install
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
```
### Environment Configuration
```bash
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/sesion"
# Redis
REDIS_URL="redis://localhost:6379"
# Anthropic AI
ANTHROPIC_API_KEY="your_anthropic_key"
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
# WhatsApp (Baileys)
WHATSAPP_SESSION_PATH="./whatsapp-sessions"
WHATSAPP_WEBHOOK_SECRET="your_webhook_secret"
# LiveKit Video
LIVEKIT_API_KEY="your_livekit_key"
LIVEKIT_API_SECRET="your_livekit_secret"
LIVEKIT_URL="wss://your-livekit-server.com"
# Payment Processors
STRIPE_SECRET_KEY="sk_test_..."
MERCADOPAGO_ACCESS_TOKEN="your_mp_token"
# AFIP (Argentina Tax Authority)
AFIP_CUIT="your_clinic_cuit"
AFIP_CERTIFICATE_PATH="./afip-cert.pem"
AFIP_PRIVATE_KEY_PATH="./afip-key.pem"
# Application
JWT_SECRET="your_jwt_secret"
APP_URL="http://localhost:3000"
```
### Database Setup
```bash
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate deploy
# Seed initial data (optional)
npx prisma db seed
```
### Start Development Server
```bash
# Start backend
npm run dev:backend
# Start frontend (separate terminal)
npm run dev:frontend
# Start both with concurrently
npm run dev
```
## Prisma Schema Key Models
```prisma
// prisma/schema.prisma
model Practitioner {
id String @id @default(uuid())
email String @unique
name String
fiscalCategory String // Monotributo, Responsable Inscripto
appointments Appointment[]
patients Patient[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Patient {
id String @id @default(uuid())
name String
email String?
phone String @unique
whatsappOptIn Boolean @default(false)
practitionerId String
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
appointments Appointment[]
journeyStage String @default("primer_contacto")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Appointment {
id String @id @default(uuid())
practitionerId String
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
patientId String
patient Patient @relation(fields: [patientId], references: [id])
startTime DateTime
endTime DateTime
sessionType String // presencial, virtual, evaluacion
status String @default("scheduled") // scheduled, confirmed, cancelled, completed
roomUrl String? // LiveKit room URL
invoiceId String? @unique
invoice Invoice? @relation(fields: [invoiceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Invoice {
id String @id @default(uuid())
practitionerId String
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
appointment Appointment?
invoiceNumber String @unique
afipCae String? // AFIP authorization code
amount Decimal
fiscalType String // A, B, C, M
paymentMethod String
paymentStatus String @default("pending")
issuedAt DateTime @default(now())
createdAt DateTime @default(now())
}
```
## Appointment Scheduling API
### Create Appointment
```typescript
// backend/src/appointments/appointments.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { WhatsAppService } from '../whatsapp/whatsapp.service';
@Injectable()
export class AppointmentsService {
constructor(
private prisma: PrismaService,
private whatsapp: WhatsAppService,
) {}
async createAppointment(data: {
practitionerId: string;
patientId: string;
startTime: Date;
sessionType: 'presencial' | 'virtual' | 'evaluacion';
}) {
// Validate time slot availability
const conflict = await this.checkConflicts(
data.practitionerId,
data.startTime,
);
if (conflict) {
throw new Error('Time slot already occupied');
}
// Calculate end time (45 min default)
const endTime = new Date(data.startTime.getTime() + 45 * 60000);
// Create appointment
const appointment = await this.prisma.appointment.create({
data: {
...data,
endTime,
status: 'scheduled',
},
include: {
patient: true,
practitioner: true,
},
});
// Send WhatsApp confirmation if patient opted in
if (appointment.patient.whatsappOptIn) {
await this.whatsapp.sendAppointmentConfirmation(appointment);
}
return appointment;
}
private async checkConflicts(practitionerId: string, startTime: Date) {
const endTime = new Date(startTime.getTime() + 45 * 60000);
return this.prisma.appointment.findFirst({
where: {
practitionerId,
status: { not: 'cancelled' },
OR: [
{
startTime: { lte: startTime },
endTime: { gt: startTime },
},
{
startTime: { lt: endTime },
endTime: { gte: endTime },
},
],
},
});
}
}
```
### Appointment Controller
```typescript
// backend/src/appointments/appointments.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AppointmentsService } from './appointments.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('appointments')
@UseGuards(JwtAuthGuard)
export class AppointmentsController {
constructor(private appointmentsService: AppointmentsService) {}
@Post()
async create(@Body() createDto: CreateAppointmentDto) {
return this.appointmentsService.createAppointment(createDto);
}
}
```
## WhatsApp Automation with Baileys
### WhatsApp Service Setup
```typescript
// backend/src/whatsapp/whatsapp.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
WAMessage,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
@Injectable()
export class WhatsAppService implements OnModuleInit {
private sock: any;
async onModuleInit() {
await this.connectToWhatsApp();
}
private async connectToWhatsApp() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_PATH,
);
this.sock = makeWASocket({
auth: state,
printQRInTerminal: true,
});
this.sock.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.connectToWhatsApp();
}
}
});
this.sock.ev.on('creds.update', saveCreds);
this.sock.ev.on('messages.upsert', this.handleIncomingMessage.bind(this));
}
async sendAppointmentConfirmation(appointment: any) {
const message = this.buildConfirmationMessage(appointment);
const phoneNumber = this.formatPhoneNumber(appointment.patient.phone);
await this.sock.sendMessage(phoneNumber, { text: message });
}
async sendReminder(appointment: any, hoursBeforeMin: number) {
const message = `🔔 Recordatorio: Tenés sesión ${
hoursBeforeMin === 2 ? 'en 2 horas' : 'mañana'
}\n\n` +
`📅 ${this.formatDateTime(appointment.startTime)}\n` +
`👨⚕️ ${appointment.practitioner.name}\n` +
`📍 ${appointment.sessionType === 'virtual' ? 'Videollamada' : 'Presencial'}\n\n` +
`Respondé SI para confirmar o NO para cancelar.`;
const phoneNumber = this.formatPhoneNumber(appointment.patient.phone);
await this.sock.sendMessage(phoneNumber, { text: message });
}
private async handleIncomingMessage({ messages }: { messages: WAMessage[] }) {
for (const msg of messages) {
if (!msg.message || msg.key.fromMe) continue;
const text = msg.message.conversation?.toLowerCase();
const phone = msg.key.remoteJid;
// Handle confirmation responses
if (text === 'si' || text === 'sí') {
await this.confirmAppointment(phone);
} else if (text === 'no') {
await this.handleCancellationRequest(phone);
}
}
}
private formatPhoneNumber(phone: string): string {
// Convert to WhatsApp format (Argentina: 549 + area + number)
return `549${phone.replace(/[^0-9]/g, '')}@s.whatsapp.net`;
}
private buildConfirmationMessage(appointment: any): string {
return `✅ Sesión agendada\n\n` +
`📅 ${this.formatDateTime(appointment.startTime)}\n` +
`👨⚕️ ${appointment.practitioner.name}\n` +
`📍 ${appointment.sessionType === 'virtual' ? 'Videollamada' : 'Presencial'}\n\n` +
`Te enviaremos recordatorios 24hs y 2hs antes de la sesión.`;
}
private formatDateTime(date: Date): string {
return new Intl.DateTimeFormat('es-AR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
}
```
### Scheduled Reminder Jobs
```typescript
// backend/src/appointments/reminder.scheduler.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { WhatsAppService } from '../whatsapp/whatsapp.service';
@Injectable()
export class ReminderScheduler {
constructor(
private prisma: PrismaService,
private whatsapp: WhatsAppService,
) {}
@Cron(CronExpression.EVERY_HOUR)
async send24HourReminders() {
const tomorrow = new Date();
tomorrow.setHours(tomorrow.getHours() + 24);
const appointments = await this.prisma.appointment.findMany({
where: {
startTime: {
gte: tomorrow,
lt: new Date(tomorrow.getTime() + 60 * 60000),
},
status: 'scheduled',
patient: { whatsappOptIn: true },
},
include: { patient: true, practitioner: true },
});
for (const apt of appointments) {
await this.whatsapp.sendReminder(apt, 24);
}
}
@Cron(CronExpression.EVERY_30_MINUTES)
async send2HourReminders() {
const twoHoursFromNow = new Date();
twoHoursFromNow.setHours(twoHoursFromNow.getHours() + 2);
const appointments = await this.prisma.appointment.findMany({
where: {
startTime: {
gte: twoHoursFromNow,
lt: new Date(twoHoursFromNow.getTime() + 30 * 60000),
},
status: 'scheduled',
patient: { whatsappOptIn: true },
},
include: { patient: true, practitioner: true },
});
for (const apt of appointments) {
await this.whatsapp.sendReminder(apt, 2);
}
}
}
```
## AFIP Electronic Billing Integration
### AFIP Service
```typescript
// backend/src/billing/afip.service.ts
import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import Afip from '@afipsdk/afip.js';
@Injectable()
export class AfipService {
private afip: any;
constructor() {
this.afip = new Afip({
Ver en GitHub