Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Gestionale per studi legali italiani: fascicoli, PEC, calendario, assistente IA sul fascicolo, generazione atti e calcoli forensi — in una sola applicazione. WhatsApp e deposito telematico esistono nel codice ma sono nascosti dall'interfaccia dall'11.09.2026, in attesa di essere stabilizzati — vedi §12. Prodotto in esercizio, non un esercizio di stile: nasce dallo Studio Legale Fussone (San Cataldo, CL) e viene usato tutti i giorni su pratiche vere.查看完整 README ↗
Gestionale per studi legali italiani: fascicoli, PEC, calendario, assistente IA sul fascicolo, generazione atti e calcoli forensi — in una sola applicazione.
WhatsApp e deposito telematico esistono nel codice ma sono nascosti dall'interfaccia dall'11.09.2026, in attesa di essere stabilizzati — vedi §12.
Prodotto in esercizio, non un esercizio di stile: nasce dallo Studio Legale Fussone (San Cataldo, CL) e viene usato tutti i giorni su pratiche vere.
L'interfaccia usa un sistema visivo unico bordeaux/oro: landing con anteprima
interattiva di dashboard, fascicolo e assistente; navigazione in vetro,
tipografia ad alta gerarchia, superfici morbide e micro-interazioni rispettose
di prefers-reduced-motion. Le stesse regole sono condivise con l'area studio
per card, campi, pulsanti e sfondo applicativo. Clienti, pratiche, dettaglio
fascicolo, Themis, incarichi, calendario, PEC e strumenti usano inoltre
intestazioni, toolbar, tabelle, stati vuoti e pannelli coerenti fra loro.
| Livello | Tecnologia |
|---|---|
| Framework | Next.js 16.3.3 (App Router, React 19.2.8) |
| Linguaggio | TypeScript 5 |
| Stile | Tailwind CSS v4 (@theme inline, token bordeaux/oro in globals.css) |
| Animazioni | motion ^13.2.0 + keyframe CSS proprietarie |
| Database / Auth / Storage / Realtime | Supabase (PostgreSQL) |
| Hosting | Vercel |
| Runtime aggiuntivi | Funzione serverless Python (generazione .docx) + servizio Node persistente (WhatsApp) |
Themis non è un solo deployable. Sono tre pezzi con cicli di vita diversi, e la separazione è deliberata:
┌────────────────────────────────────────────────────────────┐
│ VERCEL │
│ │
│ ┌───────────────────────┐ ┌──────────────────────────┐ │
│ │ Next.js (App Router) │ │ api/generate.py │ │
│ │ UI + ~66 route API │ │ Python Runtime │ │
│ │ Node.js │ │ python-docx + cryptography│ │
│ └───────────┬───────────┘ └────────────┬─────────────┘ │
│ │ cron: /api/pec/sync │ │
└──────────────┼────────────────────────────┼────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ SUPABASE │
│ PostgreSQL + RLS · Auth · Storage │
│ Realtime (7 tabelle) │
└──────────────────────────────────────────┘
▲
│ webhook HTTP autenticati (Bearer)
│
┌──────────────┴─────────────────────────────────────────────┐
│ RAILWAY / FLY.IO — whatsapp-worker/ │
│ Processo sempre acceso + volume persistente │
│ @whiskeysockets/baileys (WhatsApp Web multi-device) │
└─────────────────────────────────────────────────────────────┘
Perché il worker WhatsApp è separato: la connessione con WhatsApp deve restare aperta di continuo. Una funzione serverless vive quanto una richiesta, quindi non può reggerla. Serve un host con processo persistente e disco persistente (le credenziali Signal di ogni studio stanno su disco: senza volume, ogni riavvio costringe tutti a riscansionare il QR).
Perché la generazione .docx è in Python: python-docx sostituisce i
segnaposto dentro i run di Word — inclusi quelli spezzati su più run,
tabelle, intestazioni e piè di pagina — che in JavaScript avrebbe richiesto
di riscrivere la stessa logica. Tutto in un solo file: Vercel non
impacchetta gli altri .py della stessa cartella, quindi qualunque
from _modulo import x fallisce a runtime.
Il perno è la funzione Postgres studio_corrente(), che risponde a
"a quale studio appartengo". La stessa identica logica vale per le regole di
sicurezza del database, per l'app e per la generazione atti in Python — non
esistono tre risposte diverse alla stessa domanda.
Risolve prima l'appartenenza (studio_membri con stato = 'attivo') e
poi la proprietà (studios.id = auth.uid() AND plan IS NOT NULL). L'ordine
non è casuale: un trigger su auth.users crea una riga studios per ogni
nuovo utente, quindi anche un collaboratore ne ha una, orfana; il
plan IS NOT NULL è la seconda rete.
contesto_studio() restituisce in una sola chiamata studio, ruolo, piano e
stato dell'abbonamento — perché il layout ne faceva già una sola.
| Servizio | Uso | Stato | Configurato via |
|---|---|---|---|
| Supabase | Database, Auth, Storage, Realtime | ✅ attivo | NEXT_PUBLIC_SUPABASE_URL, ..._ANON_KEY, SUPABASE_SERVICE_ROLE_KEY |
| Vercel | Hosting Next.js + funzione Python + cron | ✅ attivo | deploy su git push |
| Anthropic (Claude) | Assistente IA — modello claude-opus-5 | ✅ attivo | ANTHROPIC_API_KEY |
| Stripe | Abbonamenti, checkout, portale cliente | ✅ attivo | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_* |
| Resend | Email transazionali (chiave licenza, richieste rimborso) | ✅ attivo | RESEND_API_KEY, RESEND_FROM_EMAIL |
| Gestori PEC (IMAP/SMTP diretto) | Posta certificata | ✅ attivo | credenziali per casella, nel database (cifrate) |
| WhatsApp via Baileys | Chat dello studio | 🚧 nascosto in UI | WHATSAPP_WORKER_URL, WHATSAPP_WORKER_SECRET |
| Google Calendar API v3 | Sincronizzazione calendario (OAuth) | ⚙️ opzionale | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
| Portale Giustizia Civile | Deep link ai registri di cancelleria | ✅ attivo | nessuna credenziale (link pubblico) |
Google Calendar degrada con grazia: senza le variabili d'ambiente, l'interfaccia lo dichiara e nasconde i pulsanti, senza rompersi. WhatsApp è nascosto a prescindere dalle variabili d'ambiente (vedi §12): il codice e le route restano, ma nessun punto dell'interfaccia vi rimanda.
Niente SaaS in mezzo: connessione diretta ai server dei gestori italiani con
imapflow (lettura), nodemailer (invio), mailparser + fast-xml-parser
(interpretazione della busta).
Il riconoscimento del tipo di messaggio non è euristico: segue il DM 2
novembre 2005, leggendo daticert.xml (radice <postacert tipo="...">). I 9
tipi previsti — posta-certificata, accettazione, non-accettazione,
presa-in-carico, avvenuta-consegna, errore-consegna,
preavviso-errore-consegna, rilevazione-virus, sconosciuto. Solo per
posta-certificata il contenuto vero si legge da postacert.eml: nella busta
il mittente è il gestore, non chi ha scritto.
Gestori preconfigurati (l'host resta comunque un campo libero):
| Gestore | IMAP | Porta |
|---|---|---|
| Aruba | imaps.pec.aruba.it | 993 |
| Namirial / Sicurezza Postale | imaps.sicurezzapostale.it | 993 |
L'host SMTP è derivato per convenzione (imaps. → smtps., porta 465), non
configurabile dall'interfaccia.
Sincronizzazione a due segnalibri: last_seen_uid sale verso i messaggi
nuovi, arretrato_fino_a scende verso lo storico. Massimo 25 messaggi per
giro. Il browser fa un giro ogni 3 minuti quando la scheda è visibile (con
turno condiviso via localStorage, perché i gestori PEC rifiutano connessioni
IMAP simultanee); se non c'è nulla di nuovo parte automaticamente un passo di
arretrato, così l'archivio si completa da solo. Il cron di Vercel fa un giro
al giorno alle 05:00 UTC anche a Themis chiuso.
Se UIDVALIDITY cambia, i segnalibri si azzerano: altrimenti si salterebbero
messaggi in silenzio.
Invio: il byte grezzo viene composto una volta sola e lo stesso identico
byte viene poi depositato nella cartella "inviata" via IMAP APPEND —
ricomporlo darebbe due Message-ID diversi. Se il deposito fallisce non è un
errore: la PEC è già partita, è un fatto giuridico compiuto.
@whiskeysockets/baileys 6.7.24, versione pinnata esatta. Imita il
protocollo WhatsApp Web multi-dispositivo, che WhatsApp non pubblica e può
cambiare senza preavviso.
I rischi sono reali e vanno detti: numero non ufficiale, possibilità di blocco del numero, libreria che insegue un protocollo altrui. Per questo il worker chiede un numero dedicato allo studio, mai il cellulare personale di un avvocato.
Il worker riceve in push da WhatsApp e chiama tre webhook distinti su Themis
(/webhook testo, /webhook-documento media, /webhook-stato spunte di
consegna). Import iniziale della cronologia: ultimi 30 giorni, massimo 300
messaggi, 150 ms di pausa tra l'uno e l'altro.
Abbinamento numero → cliente: clients.telefono è un campo libero, WhatsApp
manda un JID pulito. Si normalizza (via il non-numerico e il prefisso 39) e si
confrontano le ultime 9 cifre. La pratica si aggancia solo se ce n'è
esattamente una non archiviata: indovinare fra due sarebbe sbagliare tanto
quanto non collegare nulla.
Tre endpoint in tutto, chiamati con fetch: importare il pacchetto
googleapis porterebbe dentro i client di decine di API per niente.
Scope minimi: calendar.events + userinfo.email — lo scope pieno calendar
permetterebbe di cancellare interi calendari e cambiarne le condivisioni.
L'access_token non viene mai salvato: si riottiene da capo a ogni chiamata
decifrando il refresh token. Solo il refresh token persiste, cifrato, in una
tabella senza alcuna policy RLS (deny-all: solo service role).
Sincronizzazione in una sola direzione (Themis → Google) e fire-and-forget: un fallimento non blocca il salvataggio dell'evento. L'import Google → Themis è manuale, su richiesta, e tutto entra come tipo "altro" perché Google non sa distinguere un'udienza da un appuntamento.
Alternativa senza OAuth: un feed ICS (RFC 5545) scritto a mano, da
incollare in Google/Apple/Outlook come "Iscriviti tramite URL". Niente progetto
Google Cloud, niente verifica, nessun tetto di utenti. Include il VTIMEZONE
Europe/Rome per esteso invece di convertire in UTC: un'udienza spostata a
cavallo del cambio dell'ora finirebbe nell'ora sbagliata.
| Variabile | Obbligatoria | Serve a |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | ✅ | Client Supabase |
NEXT_PUBLIC_SUPABASE_ANON_KEY | ✅ | Client Supabase (pubblica per costruzione) |
SUPABASE_SERVICE_ROLE_KEY | ✅ | Operazioni server che devono scavalcare la RLS |
NEXT_PUBLIC_SITE_URL | ✅ | Redirect OAuth, link nelle email |
DOCUMENT_ENCRYPTION_MASTER_KEY | ✅ | Cifratura di tutti i documenti (base64) |
ANTHROPIC_API_KEY | ✅ | Assistente IA (senza: 503 leggibile) |
STRIPE_SECRET_KEY | ✅ | Checkout e portale abbonamenti |
STRIPE_WEBHOOK_SECRET | ✅ | Verifica firma webhook Stripe |
STRIPE_PRICE_MONTHLY / _SEMESTRALE / _ANNUALE | ✅ | Prezzi dei tre piani |
RESEND_API_KEY, RESEND_FROM_EMAIL | ✅ | Email transazionali |
ADMIN_EMAIL | ✅ | Unico accesso a /admin e destinatario richieste rimborso |
LICENSE_ED25519_PRIVATE_KEY_PEM | ✅ | Firma delle chiavi di licenza THM-… |
CRON_SECRET | ✅ | Autorizza il cron PEC (senza, la porta resta chiusa) |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | ⚙️ | Google Calendar via OAuth |
WHATSAPP_WORKER_URL, WHATSAPP_WORKER_SECRET | ⚙️ | Servizio WhatsApp |
THEMIS_ENABLE_WHATSAPP | ⚙️ | true riattiva pagine e API WhatsApp; assente significa disabilitato |
THEMIS_ENABLE_DEPOSITO | ⚙️ | true riattiva pagina e API Deposito; assente significa disabilitato |
| Variabile | Serve a |
|---|---|
PORT | Porta di ascolto (di solito la impone l'host) |
WHATSAPP_WORKER_SECRET | Identico a quello su Vercel: autenticazione reciproca |
VERCEL_WEBHOOK_URL | Es. https://…/api/whatsapp/webhook |
DATA_DIR | Sessioni WhatsApp — deve essere un volume persistente |
LOG_LEVEL | silent in produzione (debug logga anche il testo dei messaggi) |
.env*è in.gitignore. Nessun segreto è mai stato committato.
/dashboard e seguenti)Dashboard — onboarding guidato per i nuovi studi, sei tessere cliccabili (clienti, pratiche attive, udienze/termini a 7 giorni, prenotazioni da confermare, PEC non lette, incarichi assegnati a me), prossime scadenze, pratiche recenti, azioni rapide.
Ricerca globale — ⌘K / Ctrl+K cerca insieme clienti, pratiche,
documenti e PEC, mantenendo l'isolamento RLS dello studio.
Clienti — anagrafica persone fisiche e giuridiche, ricerca su nome/CF/P.IVA/email/telefono, filtri, ordinamento, paginazione, export CSV (con BOM UTF-8 per Excel), archiviazione morbida ed eliminazione definitiva, documenti generati per cliente, invito al portale clienti. Codice fiscale e partita IVA vengono validati con la cifra di controllo ufficiale; al salvataggio, un controllo sui duplicati confronta CF/P.IVA/PEC/email/nome con le anagrafiche già presenti e chiede una conferma esplicita prima di salvare comunque (utile anche come primo controllo di conflitto d'interessi).
Pratiche — elenco con ricerca multi-termine, filtri per stato e materia (11 tipi: ATP invalidità, sinistro, ricorso INPS, causa civile, successione, lavoro, penale, immigrazione, sovraindebitamento, mediazione condominiale, altro), export CSV. Creando una pratica di tipo sinistro nasce anche la riga dei dati sinistro.
Fascicolo (/pratiche/[id]) — la pagina più densa:
.docx scaricabileCalendario — viste giorno/settimana/mese, legenda cliccabile per famiglie di eventi, griglia costruita sulle fasce di apertura dello studio, prenotazioni dal portale clienti da accettare o rifiutare, mini-calendario e prossimi eventi, scadenze imminenti. Ogni evento creato viene copiato su Google in background, se collegato. Gli eventi importati da Google Calendar o da un file ICS vengono classificati automaticamente (udienza, termine processuale, scadenza, appuntamento) solo quando il titolo contiene indizi inequivocabili — nei casi dubbi restano "altro" e si riclassificano a mano.
PEC — schede Ricevute / Inviate / Attestazioni con contatori, selettore
del periodo mese per mese, ricerca, filtro "solo non lette", ordinamento,
paginazione. Per ogni messaggio: apertura (solo testo semplice, l'HTML non
viene renderizzato), allegati, download del .eml — l'unico documento con
valore probatorio. Nuova PEC in due fasi con schermata di conferma, con
possibilità di far scrivere la bozza a Themis. Scadenze trovate nelle PEC:
l'IA propone, l'avvocato accetta o scarta — niente entra in calendario da solo.
WhatsApp (🚧 nascosto dall'interfaccia dall'11.09.2026, vedi §12 — il codice sotto resta invariato) — messaggi con documento da collegare a un cliente (con proposta di abbinamento dell'IA e motivazione), casella chat con spunte di consegna, composizione con bozza IA, scadenze estratte dai messaggi. Reparto fascicoli: tutti gli allegati ricevuti, raggruppati per assistito.
Genera Atto — modello .docx + pratica → documento compilato. I segnaposto
automatici si riempiono dai dati (cliente, pratica, sinistro), quelli manuali
si chiedono, con proposte dai valori già usati.
Deposito (🚧 nascosto dall'interfaccia dall'11.09.2026, vedi §12 — il codice sotto resta invariato) — non costruisce la busta PCT e non firma nulla. Produce due cose: una lista di controllo pre-deposito (ufficio, controparte, documenti, dati difensore, PEC) e un prontuario ricalcato sulle schermate di SLpct (5.1 dati generali, 5.2 contributo unificato, 5.3 partecipanti, 5.4 avvocato, 5.6 allegati, 5.7 attestazione di conformità), con copia riga per riga. Più un pacchetto .zip con atto principale e allegati rinominati in ordine. I file firmati fuori (Dike, ArubaSign, SLpct) si ricaricano accanto agli originali.
Calcolo Danno — tre calcolatori: Tabelle di Milano 2024, art. 139 Cod. Ass. (micropermanenti 1-9%), TUN art. 138 (macropermanenti 10-100%).
Parcelle — parametri forensi su 7 tipi di procedimento, fase per fase, con spese forfettarie, Cassa Forense e IVA.
Patrocinio Stato — tutte le pratiche a spese dello Stato con lo stato di avanzamento calcolato ("in attesa del decreto", "decreto arrivato: emettere fattura", …).
Giustizia Civile — elenco delle pratiche con R.G. e link precompilato al portale del Ministero.
Incarichi — assegnazione fra membri, priorità, scadenze, presa in carico, passaggio, riapertura. Storico scritto dai trigger del database.
Impostazioni — guida "Come usare Themis" scaricabile in PDF (presente anche in Domande frequenti), password, verifica in due passaggi (2FA), backup dei dati dello studio (solo titolare), abbonamento e rimborso, scheda per l'elenco pubblico studi, carta intestata, tipografia dei documenti, dati del difensore, orari per il portale, Google Calendar (OAuth
Collaboratori e Registro attività — solo titolare.
/portale)Il cliente non entra mai nell'area studio. Vede soltanto i propri appuntamenti, la prenotazione di nuovi slot (con promemoria email a scelta) e i documenti che lo studio gli ha chiesto, con il caricamento.
/admin)Riservato all'unico utente la cui email coincide con ADMIN_EMAIL. KPI,
richieste di rimborso, tetti di spesa IA per piano, consumo per abbonato,
generazione chiavi di licenza, creazione studi, gestione scadenze e stati.
/ (landing con carosello sponsor), /studi (elenco pubblico degli studi
con piano annuale che hanno scelto di farsi trovare), /sponsor, /privacy,
/politica-rimborsi, /accedi, /registrati, /attiva, /unisciti,
/reimposta-password, /account-sospeso.
Tre ruoli applicativi più l'amministratore (identificato dall'email, non dal ruolo).
| Ambito | Cosa |
|---|---|
| Pagine (gate server-side) | /collaboratori, /attivita |
| PEC | inviare una PEC ("è un atto giuridico"), aggiungere/rimuovere caselle |
| Collaboratori | invitare, disattivare, rimuovere |
| Google Calendar | collegare, scollegare, mettere in pausa, importare |
| WhatsApp (sezione nascosta, vedi §12) | collegare e scollegare il numero |
| Calendario | creare/rigenerare/spegnere il link ICS pubblico |
| Licenze | attivare o riscattare una chiave |
| Interfaccia | blocco Abbonamento e Elenco studi, scheda "Studio" delle notifiche, toggle "vedi tutto lo studio" negli incarichi, eliminazione di un incarico |
Clienti, pratiche, fascicoli, calendario, lettura PEC, tutto l'assistente IA, Genera Atto, Calcolo Danno, Parcelle, Patrocinio, Giustizia Civile, e la parte non riservata delle impostazioni. Chat WhatsApp e invio messaggi e Deposito seguirebbero la stessa regola, ma sono attualmente nascosti (§12).
Le voci di menu nascoste sono una cortesia: il controllo vero è sempre lato server, nelle route API e nelle policy del database. Questo vale anche per WhatsApp e Deposito: nascosti dal menu, ma le route restano raggiungibili direttamente da URL e la logica di permessi sotto è invariata.
| Piano | Prezzo | Collaboratori (oltre al titolare) | Assistente IA |
|---|---|---|---|
| Mensile | 100 €/mese | 1 | ❌ non incluso |
| Semestrale | 500 €/6 mesi | 3 | ✅ con limite mensile |
| Annuale | 1.100 €/anno | 5 | ✅ margine più ampio + spazio sponsor gratuito |
Le migrazioni non contengono lo schema completo: lo schema di base è stato
creato a mano su Supabase prima che esistesse la cartella supabase/migrations/
e non è mai stato versionato. Da agosto 2026 ogni modifica si scrive prima in
un file numerato e poi si esegue.
| Area | Tabelle |
|---|---|
| Studio e accessi | studios, studio_membri, studio_settings, issued_licenses |
| Anagrafiche e pratiche | clients, matters, sinistri, testimoni, patrocini_spese_stato, matter_parti, verifiche_cliente, calcoli_pratica |
| Documenti | documenti (con versione, documento_padre_id, categoria, tags, hash_sha256, caricato_da, dimensione_bytes dalla 037), templates, template_placeholders, document_requests |
| IA — cronologia | themis_conversazioni, themis_messaggi |
| Calendario | eventi, appointments, availability_rules, google_calendar_account, google_calendar_credenziali |
| PEC | pec_account, pec_credenziali, pec_cartelle, pec_messaggi, pec_proposte |
whatsapp_account, whatsapp_messaggi, whatsapp_proposte, whatsapp_bozza_feedback | |
| IA | ai_utilizzo, limiti_assistente, stili_atto, atto_feedback |
| Lavoro e tracciabilità | incarichi, incarichi_storico, notifiche |
| Portale clienti | portal_clients, portal_invites |
| Fatturazione | stripe_webhook_events |
| Sito pubblico | sponsors, sponsor_richieste |
| Funzione | Scopo |
|---|---|
studio_corrente() | Il perno del multi-tenant: a quale studio appartengo |
ruolo_corrente(), e_titolare() | Ruolo dell'utente corrente |
contesto_studio() | Studio + ruolo + piano + abbonamento, in una sola chiamata |
invito_portale(p_code) | Legge una sola riga di portal_invites dato il codice |
cliente_portale_corrente() | Il client_id del cliente loggato nel portale |
redeem_license(p_license_id) | Riscatto di una chiave THM-… |
get_taken_slots(...) | Slot già occupati, per il portale |
genera_notifiche_scadenze() | Notifiche di scadenza (chiamata all'apertura della campanella: non c'è pg_cron) |
Bucket unico documents, con prefissi per tipo: documenti/, templates/,
letterheads/, pec/. Ogni file è cifrato prima dell'upload (estensione
.enc) e ogni accesso passa dal service role — mai direttamente dal browser.
Sette tabelle pubblicate: notifiche, incarichi, incarichi_storico,
document_requests, appointments, pec_messaggi, pec_account.
Sulle prime cinque è impostato replica identity full: senza, su UPDATE e
DELETE PostgreSQL trasmette la sola chiave primaria, Realtime non può
verificare le policy RLS sulla riga vecchia e non consegna l'evento a
nessuno.
create policy <nome> on public.<tabella>
for all to authenticated
using (studio_id = public.studio_corrente())
with check (studio_id = public.studio_corrente());
L'isolamento fra studi passa interamente dalla funzione studio_corrente().
Le regole restrittive stanno nei trigger, non nelle policy. Le policy
permissive si sommano in OR: una policy più stretta non può togliere ciò che
una più larga già concede. Perciò "solo il titolare elimina" e "si assegna solo
a un membro attivo dello stesso studio" sono trigger BEFORE che sollevano
eccezione.
Tabelle deny-all (RLS attiva, zero policy — leggibili solo con service
role): pec_credenziali, google_calendar_credenziali,
stripe_webhook_events. Ogni integrazione con credenziali usa due tabelle:
una leggibile per l'interfaccia, una segreta.
Notifiche e storico non sono falsificabili dal browser: sulle rispettive tabelle non esiste permesso di INSERT, li scrivono solo i trigger.
Attivabile da Impostazioni → Sicurezza, usa il supporto MFA nativo di
Supabase Auth (supabase.auth.mfa.*, TOTP compatibile con Google
Authenticator, Microsoft Authenticator, 1Password) — nessuna libreria di
crittografia scritta in casa, la verifica del codice avviene lato Supabase,
non nel browser.
L'enforcement è a due livelli, non solo alla schermata di login:
AccediClient.tsx).contestoStudio(): entrambi confrontano
getAuthenticatorAssuranceLevel() — se l'account richiede aal2 ma la
sessione corrente è ferma ad aal1 (es. un cookie di sessione precedente
all'attivazione della 2FA), la richiesta viene rimandata al login invece
di procedere. Senza questo doppio controllo, una sessione già aperta
prima di attivare la 2FA continuerebbe a funzionare senza mai chiedere il
secondo fattore.Da Impostazioni → Sicurezza (solo titolare), /api/studio/export genera uno
.zip con un file JSON per tabella (clienti, pratiche, parti, verifiche,
sinistri, testimoni, calendario, incarichi e storico, documenti — solo
metadati, patrocini, calcoli salvati, conversazioni Themis), più un
manifest.json con conteggi e avvisi. Ogni tabella è filtrata per
studio_id esplicitamente nella query, non solo per RLS. Non include i
file binari cifrati né le credenziali PEC/Google — lo dichiara lo stesso
manifest.
portal_invites (chiusa il 31.08.2026)Vale la pena raccontarla, perché è un tranello classico della RLS.
La policy si chiamava "Chiunque legge un invito dato il codice" ed era
scritta for select to public using (true). Il nome esprimeva l'intenzione
giusta, ma una policy non può filtrare in base a ciò che c'era nella query:
il database non sa che il codice era nel WHERE. USING (true) significa
"tutte le righe, sempre", e to public comprende anon.
Siccome la chiave anonima di Supabase è pubblica per costruzione — sta nel JavaScript che ogni browser scarica — chiunque su internet poteva leggere l'intera tabella: nomi ed email dei clienti di tutti gli studi, e i codici degli inviti non ancora usati, con cui ci si sarebbe potuti registrare al posto di quel cliente.
La chiusura in tre passi, nell'ordine (per non far mai smettere di funzionare
il portale): migrazione 007 che aggiunge le funzioni → deploy del codice che le
usa → migrazione 008 che elimina la policy. La soluzione strutturale è che il
codice diventa argomento di una funzione SECURITY DEFINER invece che
condizione di una policy: invito_portale(p_code) restituisce una riga sola e
tre sole colonne, mai code, studio_id o client_id.
HKDF-SHA256(masterKey, salt='themis-doc-key', info=studio_id, 32)IV (12) || CIPHERTEXT || TAG (16)DOCUMENT_ENCRYPTION_MASTER_KEY: mai nel
database, mai nel browsersrc/lib/crypto/docEncryption.ts, Python in api/generate.py)doc.studio_id), non con
quello di chi scarica: è il documento a sapere con quale chiave è stato
cifrato — e resta corretto quando a caricare è stato un collaboratore o un
cliente dal portaleSe la master key si perde, i documenti sono irrecuperabili. Non c'è escrow, né backup della chiave, né possibilità di ri-cifratura: ogni blob nello Storage diventa rumore. È una scelta, e va conosciuta.
Formato THM-XXXXX-XXXXX-…: payload 1|{license_id}|{expires_at}|{plan}
firmato Ed25519, il tutto in base32 RFC4648 senza padding. Il client
estrae il solo license_id senza verificare la firma; la verifica vera
(chiave esistente, non già usata) avviene nella funzione Postgres
redeem_license().
| Modello | claude-opus-5 (definito in un solo punto, src/lib/ai/claude.ts) |
| Prezzo input | $5 / MTok |
| Prezzo output | $25 / MTok |
| Cache lettura / scrittura | $0,50 / $6,25 per MTok |
Si ragiona in dollari e mai in euro: Anthropic fattura in dollari, e un cambio scritto nel codice mostrerebbe un consumo che non torna mai con la console.
Ritentativi con backoff esponenziale (da 1500 ms) su 429, 500, 502, 503, 529; messaggi di errore tradotti in italiano leggibile ("Themis è momentaneamente sovraccarico", non uno status code).
Il consumo si registra in ai_utilizzo in millesimi di dollaro, per studio
e per mese. Il tetto viene dalla tabella limiti_assistente, regolabile dal
pannello admin senza pubblicare codice.
Valori attualmente in produzione:
| Piano | Tetto/mese | In pratica |
|---|---|---|
monthly | $0 | assistente non incluso |
semestrale | $3 | ~30 domande |
annuale | $18 | ~180 domande |
Due principi nel codice:
residuoPct, esaurito), mai le cifre.| Funzione | Cosa fa |
|---|---|
domanda | Domande sul fascicolo, multi-turno, con citazione di documento e pagina |
bozza | Prima stesura di un atto + .docx salvato nel fascicolo |
scadenze | Estrae scadenze dalle PEC (5 per giro) |
pec | Scrive la bozza di una PEC |
whatsapp | Bozza di risposta a un messaggio |
whatsapp-scadenze | Estrae scadenze dai messaggi (10 per giro) |
whatsapp-abbina | Propone il cliente a cui agganciare un documento ricevuto |
I PDF vengono passati nativamente all'API (non convertiti in testo): così
si conserva l'impaginazione e si può citare la pagina. I .docx vengono letti
paragrafo per paragrafo, perché Word spezza abitualmente una parola su più tag.
Massimo 10 documenti per richiesta, e solo quelli che l'utente ha scelto: nessun invio silenzioso di un intero fascicolo a un servizio esterno.
[DA COMPLETARE: …],
[NORMA DA VERIFICARE], [EVENTUALE RICHIAMO GIURISPRUDENZIALE — a cura del difensore]. Un segnaposto onesto è sempre preferibile a un dato inventato:
il primo si nota, il secondo no.Tre canali di feedback (bozze di atti, bozze WhatsApp, proposte di scadenza scartate) con la stessa logica: un "sì" non lascia traccia, solo un "no" con nota entra nel prompt successivo — le ultime cinque, per studio e per tipo.
Gli scheletri di atto stanno in tabella (stili_atto), non nel codice, e
contengono zero fatti: solo sezioni, formule di rito e segnaposto. Quelli dello
studio precedono quelli di sistema. I blocchi marcati
FORMULA — DA RIPRODURRE ALLA LETTERA (procura, dichiarazione sostitutiva,
relata di notifica) vanno riprodotti parola per parola: una parola tolta è un
potere che non c'è.
Ogni modulo dichiara la propria fonte, l'edizione e i limiti.
| Modulo | Fonte |
|---|---|
| Parcelle | D.M. 55/2014 come sostituito dal D.M. 147/2022 (in vigore dal 23.10.2022) — 7 tabelle, 6 scaglioni, fasi separate, +15% forfettario, +4% CPA, +22% IVA |
| Tabelle di Milano | Edizione 2024 (Osservatorio Giustizia Civile Milano, 21.05.2024) — punto 1-100, incremento sofferenza 25-50%, demoltiplicatore età, ITT €115/giorno |
| Micropermanenti | Art. 139 Cod. Ass., coefficienti fissati dalla legge, valori D.M. 20.07.2026 (punto base €988,45; ITT €57,64) |
| Macropermanenti | TUN art. 138 Cod. Ass., D.P.R. 12/2025 — Tavola 1.A/1.B (biologico ed età), Tavola 2 (morale), personalizzazione fino al 30% |
| Scadenze legali | 20 termini in 10 categorie, ciascuno con riferimento normativo, e sospensione feriale (L. 742/1969, +31 giorni per ogni agosto attraversato) applicata solo dove pertinente |
Il filtro delle scadenze per materia è deliberatamente generoso: una scadenza non calcolata è una scadenza persa, quindi si può sempre vedere tutto.
⚠️ I valori sono hardcoded e non esiste alcun meccanismo di aggiornamento automatico né di allerta. Le Tabelle di Milano vengono aggiornate periodicamente secondo ISTAT: prima di un uso professionale va verificato che non sia uscita un'edizione più recente. Non esistono test automatici su questi moduli (vedi §12).
Deep link al portale del Ministero (servizipst.giustizia.it). Il portale non
ha API e la selezione è una cascata di select JavaScript, ma la pagina di
destinazione accetta gli stessi parametri anche in query string — verificato a
mano.
Mappati 23 uffici siciliani (15 tribunali, 4 corti d'appello, 4 giudici di
pace) e i registri deducibili dalla materia (CC, LAV, FALL). Dove la
materia non lo suggerisce con certezza il registro si omette: lasciare che
l'avvocato lo scelga è meglio che indovinare male.
Il CAPTCHA resta da risolvere a mano e non si tenta di aggirarlo: è un controllo del Ministero, non un ostacolo tecnico.
npm install
# crea .env.local con le variabili elencate al §3
# (non esiste un file di esempio committato: .env* è in .gitignore)
npm run dev # http://localhost:3000
Verifica prima di ogni commit:
npx tsc --noEmit -p tsconfig.json
npm run build
npm run devnon basta per verificare il layout. Il CSS in modalità sviluppo viene servito a pezzi e produce falsi positivi sui problemi responsive che nel bundle compilato non esistono. Per un controllo visivo attendibile:npm run build && npm start.
Worker WhatsApp:
cd whatsapp-worker
npm install && cp .env.example .env
npm run dev
Webapp: git push sul branch main → Vercel costruisce e pubblica. Non si
usa vercel --prod.
Worker WhatsApp: container Docker su Railway o Fly.io, con volume
persistente montato su /app/dati.
Cron: uno solo, in vercel.json — /api/pec/sync alle 05:00 UTC.
L'estrazione delle scadenze non è schedulata: parte su richiesta dell'utente.
Si applicano a mano nell'SQL Editor di Supabase: non c'è la CLI collegata al progetto, e nessuno strumento le esegue al deploy. Convenzione: un file numerato per modifica, in testa cosa fa e perché, e mai modificare un file già applicato — se serve un correttivo se ne scrive uno nuovo.
Stato verificato in produzione l'08.09.2026 (interrogando direttamente il
database, perché supabase/migrations/README.md era rimasto indietro):
| Migrazioni | Stato |
|---|---|
| 001–011 | ✅ applicate |
| 012, 013 (IA e tetti) | ✅ applicate — il README delle migrazioni le dà ancora per non applicate |
| 014–025, 027–035 | ✅ applicate (tabelle e colonne presenti) |
| 026 (dati del difensore per il deposito) | ❌ non applicata — vedi §12 |
| 036 (doppia notifica PEC) | ✅ applicata (13.09.2026) |
| 037 (parti pratica, verifiche, versioni documentali, conversazioni Themis, pianificazione, calcoli salvati) | ✅ applicata (13.09.2026) |
| 038 (corregge l'eliminazione di clienti/pratiche/documenti e la versione dei documenti) | ⚠️ da applicare — vedi §12.12 |
Elenco onesto di ciò che è stato verificato, non di ciò che si sospetta.
Verificato in produzione: le colonne avvocato_cognome, avvocato_nome,
avvocato_codice_fiscale, avvocato_indirizzo, avvocato_cap,
avvocato_citta, avvocato_provincia non esistono in studio_settings,
ma il codice le legge e le scrive in due punti:
src/app/(studio)/impostazioni/page.tsx (blocco "Dati del difensore") e
src/app/(studio)/deposito/PreparaDeposito.tsx (sezione 5.4 del prontuario
SLpct).
Effetto: il salvataggio di quei campi fallisce e la sezione 5.4 del
prontuario resta vuota. Rimedio: eseguire
supabase/migrations/026_dati_professionista_deposito.sql.
Verificato in produzione: ogni PEC in arrivo genera due righe in
notifiche, una per trigger:
trg_notifica_pec (migrazione 010) → tipo pec, destinatario NULL, nessuna
chiave di unicitàpec_messaggi_notifica (migrazione 024) → tipo pec_ricevuta, destinatario
valorizzato, chiave_unicita = 'pec:'||idLa migrazione 036_correzioni_pec_ai.sql elimina il trigger precedente e tiene
la notifica idempotente introdotta dalla 024. Deve essere applicata in produzione.
bozza — corretto nel codice/api/themis/pec registra ora il consumo come pec; bozza resta riservato
agli atti. Non è necessaria una modifica allo schema perché il campo è testuale.
Tabelle di Milano, TUN, art. 139, parametri forensi e scadenze producono numeri che finiscono in atti e parcelle, e non hanno alcuna suite di test. Le verifiche dichiarate nei commenti sono state fatte a mano contro i PDF ufficiali.
L'autenticazione fra worker e webapp è un solo Authorization: Bearer <segreto condiviso>, in entrambe le direzioni. Nessuna firma HMAC, nessun timestamp,
nessuna protezione anti-replay (l'equivalente di X-Hub-Signature-256 di Meta
non esiste qui).
L'invio ora controlla la risposta, ritenta gli errori transitori ed esegue l'escaping dei valori interpolati. Le licenze create dal webhook Stripe sono deterministiche per evento: un retry invia la stessa chiave valida.
Baileys imita un protocollo non pubblicato. Se le connessioni smettono di funzionare tutte insieme, il primo posto dove guardare è il repository della libreria, non questo codice — e la versione non va aggiornata alla cieca.
Ogni risposta non-OK diventa direttamente un'eccezione. Un fallimento di
sincronizzazione finisce solo in console.error: non c'è ancora un posto
nell'interfaccia dove mostrarlo.
012_ai.sql descrive costo_millesimi come "millesimi di euro"; tutto il
codice ragiona in dollari. Il commento è sbagliato, non il codice.
Entrambe le funzionalità funzionavano in modo instabile, quindi sono state disabilitate per impostazione predefinita — non rimosse dal codice. È una decisione deliberata, in attesa che tornino affidabili, non un bug.
Cosa è cambiato: rimosse le voci /whatsapp, /whatsapp/documenti e
/deposito da NAV in src/app/(studio)/layout.tsx e dai relativi gruppi
in src/app/(studio)/SidebarNav.tsx; tolto il render di
<ImpostazioniWhatsapp /> da src/app/(studio)/impostazioni/page.tsx
(import commentato, non cancellato); ripulite le menzioni pubbliche in
src/app/page.tsx, src/app/layout.tsx (meta description) e
src/app/(studio)/domande-frequenti/page.tsx.
Il codice resta presente: le route (src/app/(studio)/whatsapp/,
src/app/(studio)/deposito/), le API (src/app/api/whatsapp/*,
src/app/api/themis/whatsapp*, src/app/api/pratiche/[id]/pacchetto-deposito/),
le tabelle del database, i trigger, il worker WhatsApp e le policy RLS sono
tutti intatti. Il proxy restituisce però 404 alle API e rimanda le pagine
alla dashboard finché i rispettivi feature flag non sono esplicitamente attivi.
Per riattivarle: impostare THEMIS_ENABLE_WHATSAPP=true e/o
THEMIS_ENABLE_DEPOSITO=true, poi ripristinare le rispettive voci in
NAV/GRUPPI e il render di ImpostazioniWhatsapp.
Guida utente: contestualmente è stata aggiunta una guida "Come usare
Themis" in PDF (public/guide/come-usare-themis.pdf, generata senza alcun
riferimento a WhatsApp o Deposito), scaricabile da Impostazioni e da Domande
frequenti — va rigenerata e aggiornata quando le due funzionalità
rientreranno nell'interfaccia.
Rilascio che aggiunge: verifica in due passaggi (§7), backup dati esportabile (§7), conversazioni Themis persistenti nel fascicolo, controlli professionali (conflitto d'interessi, adeguata verifica, privacy, mandato), parti e professionisti collegati alla pratica, calcoli di danno/parcella salvabili nel fascicolo, versioning dei documenti, validazione formale di CF/P.IVA con controllo duplicati, classificazione automatica prudente degli eventi importati da calendario esterno.
Ordine di deploy obbligatorio (diversamente da una migrazione qualunque,
qui l'ordine conta): prima 037_studio_operativo_v2.sql su Supabase, poi il
deploy del codice. Il codice tollera comunque l'ordine inverso per una
finestra breve — l'upload documenti e i form di parti/verifiche/calcoli
rilevano l'errore Postgres di colonna/tabella mancante e degradano con un
avviso invece di rompersi — ma non è la sequenza pensata.
Funzioni esplicitamente non presentate come operative in questo rilascio: firma elettronica qualificata, conservazione a norma, fatturazione elettronica, notifiche push. Richiedono un fornitore esterno scelto e configurato a parte.
Trovati testando l'app end-to-end come farebbe un avvocato, non dalla sola lettura del codice.
Eliminare un cliente, una pratica o un documento falliva sempre.
notifica_eliminazione() (migrazione 010) è un trigger condiviso da tre
tabelle e leggeva old.nome_file (solo su documenti) e
old.cognome/old.nome/old.ragione_sociale (solo su clients)
direttamente sul record. PL/pgSQL verifica l'esistenza del campo sul tipo di
riga reale anche nei rami del CASE non presi: eliminare una riga da una
tabella priva di quella colonna falliva con un errore tecnico ("record
"old" has no field..."), mostrato in un alert() del browser, non un
messaggio comprensibile. Effetto pratico: il pulsante "Elimina" su
cliente/pratica/documento non ha mai funzionato, senza che l'interfaccia
lo segnalasse chiaramente. Corretto leggendo i campi da to_jsonb(old),
che su una chiave assente restituisce NULL invece di un errore.
La versione dei documenti mostrata nel fascicolo non era quella dichiarata. Doveva essere "quante volte è già stato caricato un file con questo nome in questa pratica". Due problemi distinti, trovati in sequenza:
src/app/api/documenti/upload/route.ts (un conteggio lato
client prima dell'insert, filtrato per nome file) restava comunque
soggetto a race condition fra upload concorrenti — corretto spostandolo in
un trigger BEFORE INSERT sul database, con un advisory lock sulla
coppia pratica+nome file: sempre atomico, indipendente da chi chiama
l'insert.src/app/(studio)/pratiche/[id]/page.tsx: la query che carica i documenti
del fascicolo non selezionava nemmeno la colonna versione, e
l'interfaccia mostrava documenti.length - indice (la posizione nella
lista ordinata per data) spacciandola per il numero di versione. Corretto
leggendo il campo reale dal database, con un'unica avvertenza: se lo
studio non ha ancora applicato la 037 la colonna non c'è, e in quel caso
l'indicazione di versione viene omessa invece di mostrare un numero
inventato.Software proprietario. Tutti i diritti riservati. Il codice contiene logica commerciale (chiavi di licenza, tetti di spesa, margini) e non è destinato alla distribuzione.
LEGAL_ENTITY_NAME, LEGAL_ENTITY_ADDRESS, LEGAL_ENTITY_VAT |
| ✅ |
| Identità del fornitore mostrata nei documenti legali |
PRIVACY_CONTACT_EMAIL | ✅ | Contatto privacy; senza i quattro dati legali le registrazioni restano sospese |