- name
- sesion-mental-health-platform
- description
- AI-powered mental health practice management platform for psychologists with scheduling, WhatsApp automation, AFIP billing, video consultations, and Claude AI integration
- triggers
- ["how do I set up the Sesión mental health platform","configure WhatsApp automation for patient appointments","integrate AFIP electronic invoicing in Sesión","set up video consultations with LiveKit","use Claude AI for clinical notes in Sesión","configure MercadoPago payment integration","implement patient journey workflows","troubleshoot Sesión WhatsApp connection"]
# Sesión Mental Health 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 (via Baileys), AFIP-compliant electronic invoicing, secure video consultations (LiveKit), and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6. Built with SvelteKit 5, NestJS, Prisma, and TypeScript.
## Installation
### Prerequisites
```bash
# Required versions
node >= 18.0.0
pnpm >= 8.0.0
postgresql >= 14.0
redis >= 7.0
```
### Clone and Setup
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
# Install dependencies
pnpm install
# Setup environment variables
cp .env.example .env
```
### Environment Configuration
```bash
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/sesion"
REDIS_URL="redis://localhost:6379"
# Anthropic AI
ANTHROPIC_API_KEY="sk-ant-..."
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
# WhatsApp (Baileys)
WHATSAPP_SESSION_DIR="./sessions"
WHATSAPP_QR_TIMEOUT=60000
# LiveKit Video
LIVEKIT_API_KEY="${LIVEKIT_API_KEY}"
LIVEKIT_API_SECRET="${LIVEKIT_API_SECRET}"
LIVEKIT_WS_URL="wss://your-livekit.cloud.livekit.cloud"
# Payment Gateways
MERCADOPAGO_ACCESS_TOKEN="${MERCADOPAGO_ACCESS_TOKEN}"
STRIPE_SECRET_KEY="${STRIPE_SECRET_KEY}"
# AFIP Integration
AFIP_CUIT="${AFIP_CUIT}"
AFIP_CERT_PATH="./certs/afip.crt"
AFIP_KEY_PATH="./certs/afip.key"
AFIP_PRODUCTION=false
# Application
JWT_SECRET="${JWT_SECRET}"
APP_URL="http://localhost:5173"
API_URL="http://localhost:3000"
```
### Database Setup
```bash
# Run migrations
pnpm prisma migrate dev
# Seed initial data
pnpm prisma db seed
```
### Start Development Servers
```bash
# Frontend (SvelteKit)
pnpm dev
# Backend (NestJS)
pnpm api:dev
# Full stack
pnpm dev:full
```
## Architecture Overview
```
sesion/
├── apps/
│ ├── web/ # SvelteKit 5 frontend
│ ├── api/ # NestJS backend
│ └── worker/ # Background job processor
├── packages/
│ ├── db/ # Prisma schema & migrations
│ ├── ai/ # Claude AI orchestration
│ ├── whatsapp/ # Baileys integration
│ ├── video/ # LiveKit wrapper
│ ├── billing/ # AFIP invoicing
│ └── shared/ # Common types & utils
└── prisma/
└── schema.prisma
```
## Core Modules
### 1. Appointment Scheduling
#### Create Appointment (NestJS Controller)
```typescript
// apps/api/src/appointments/appointments.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AppointmentsService } from './appointments.service';
import { CreateAppointmentDto } from './dto/create-appointment.dto';
@Controller('appointments')
@UseGuards(JwtAuthGuard)
export class AppointmentsController {
constructor(private readonly appointmentsService: AppointmentsService) {}
@Post()
async create(@Body() dto: CreateAppointmentDto) {
return this.appointmentsService.create(dto);
}
}
```
#### Appointment Service with Conflict Detection
```typescript
// apps/api/src/appointments/appointments.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { PrismaService } from '@sesion/db';
import { addMinutes, isWithinInterval } from 'date-fns';
@Injectable()
export class AppointmentsService {
constructor(private prisma: PrismaService) {}
async create(dto: CreateAppointmentDto) {
// Check for conflicts
const conflicts = await this.prisma.appointment.findMany({
where: {
practitionerId: dto.practitionerId,
status: { not: 'CANCELLED' },
scheduledAt: {
gte: dto.scheduledAt,
lt: addMinutes(dto.scheduledAt, dto.durationMinutes || 45)
}
}
});
if (conflicts.length > 0) {
throw new ConflictException('Time slot already booked');
}
// Create appointment
const appointment = await this.prisma.appointment.create({
data: {
...dto,
status: 'SCHEDULED'
},
include: {
patient: true,
practitioner: true
}
});
// Trigger WhatsApp confirmation
await this.whatsappService.sendConfirmation(appointment);
return appointment;
}
async findUpcoming(practitionerId: string) {
return this.prisma.appointment.findMany({
where: {
practitionerId,
scheduledAt: { gte: new Date() },
status: { in: ['SCHEDULED', 'CONFIRMED'] }
},
include: { patient: true },
orderBy: { scheduledAt: 'asc' }
});
}
}
```
#### Frontend: Appointment Calendar (Svelte 5)
```svelte
<!-- apps/web/src/routes/agenda/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
let appointments = $state([]);
let selectedDate = $state(new Date());
let loading = $state(false);
async function loadAppointments() {
loading = true;
const res = await fetch('/api/appointments/upcoming', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
appointments = await res.json();
loading = false;
}
async function createAppointment(event: CustomEvent) {
const res = await fetch('/api/appointments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(event.detail)
});
if (res.ok) {
await loadAppointments();
}
}
onMount(loadAppointments);
</script>
<div class="agenda-container">
<header>
<h1>Agenda</h1>
<button onclick={() => goto('/appointments/new')}>
Nueva Cita
</button>
</header>
{#if loading}
<div class="skeleton"></div>
{:else}
<div class="appointments-grid">
{#each appointments as apt}
<div class="appointment-card" class:urgent={apt.isUrgent}>
<time>{new Date(apt.scheduledAt).toLocaleString('es-AR')}</time>
<h3>{apt.patient.name}</h3>
<span class="type">{apt.type}</span>
<span class="status">{apt.status}</span>
</div>
{/each}
</div>
{/if}
</div>
```
### 2. WhatsApp Automation (Baileys)
#### WhatsApp Connection Service
```typescript
// packages/whatsapp/src/whatsapp.service.ts
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
makeInMemoryStore
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class WhatsAppService {
private sock: any;
private readonly logger = new Logger(WhatsAppService.name);
async connect() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_DIR || './sessions'
);
this.sock = makeWASocket({
auth: state,
printQRInTerminal: true
});
this.sock.ev.on('creds.update', saveCreds);
this.sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
this.logger.log('QR Code generated, scan with WhatsApp');
}
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !==
DisconnectReason.loggedOut;
if (shouldReconnect) {
this.logger.log('Reconnecting...');
this.connect();
}
} else if (connection === 'open') {
this.logger.log('WhatsApp connected successfully');
}
});
this.sock.ev.on('messages.upsert', async ({ messages }) => {
await this.handleIncomingMessage(messages[0]);
});
}
async sendReminder(appointment: Appointment) {
const patientNumber = appointment.patient.phone.replace(/\D/g, '') + '@s.whatsapp.net';
const message = `Hola ${appointment.patient.name}! 👋\n\n` +
`Te recordamos tu sesión programada:\n` +
`📅 ${format(appointment.scheduledAt, "dd/MM/yyyy 'a las' HH:mm", { locale: es })}\n` +
`👨⚕️ Con: ${appointment.practitioner.name}\n\n` +
`Por favor confirma tu asistencia respondiendo SI o NO.`;
await this.sock.sendMessage(patientNumber, { text: message });
await this.prisma.whatsappMessage.create({
data: {
appointmentId: appointment.id,
to: patientNumber,
content: message,
type: 'REMINDER',
status: 'SENT'
}
});
}
async handleIncomingMessage(message: any) {
if (!message.message?.conversation) return;
const text = message.message.conversation.toLowerCase();
const from = message.key.remoteJid;
// Check for confirmation replies
if (['si', 'sí', 'confirmo'].includes(text)) {
await this.confirmAppointment(from);
} else if (['no', 'cancelar'].includes(text)) {
await this.requestCancellation(from);
}
// Crisis keyword detection
if (this.detectCrisisKeywords(text)) {
await this.alertPractitioner(from, text);
}
}
private detectCrisisKeywords(text: string): boolean {
const crisisKeywords = [
'suicidio', 'matarme', 'terminar con todo',
'no puedo más', 'crisis', 'emergencia'
];
return crisisKeywords.some(kw => text.includes(kw));
}
}
```
#### Automated Reminder Scheduler
```typescript
// apps/worker/src/jobs/reminder.job.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaService } from '@sesion/db';
import { WhatsAppService } from '@sesion/whatsapp';
import { subHours } from 'date-fns';
@Injectable()
export class ReminderJob {
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: {
scheduledAt: {
gte: tomorrow,
lt: new Date(tomorrow.getTime() + 60 * 60 * 1000) // +1 hour window
},
status: 'SCHEDULED',
reminderSent24h: false
},
include: { patient: true, practitioner: true }
});
for (const apt of appointments) {
await this.whatsapp.sendReminder(apt);
await this.prisma.appointment.update({
where: { id: apt.id },
data: { reminderSent24h: true }
});
}
}
}
```
### 3. AFIP Electronic Invoicing
#### Invoice Generation Service
```typescript
// packages/billing/src/afip.service.ts
import { Injectable } from '@nestjs/common';
import Afip from '@afipsdk/afip.js';
import { PrismaService } from '@sesion/db';
@Injectable()
export class AfipService {
private afip: Afip;
constructor(private prisma: PrismaService) {
this.afip = new Afip({
CUIT: process.env.AFIP_CUIT,
cert: process.env.AFIP_CERT_PATH,
Ver en GitHub