Documentazione — Come funziona
Ultimo aggiornamento: 6 agosto 2026
1. Panoramica del flusso
Un appuntamento viene creato da un organizzatore connesso, poi inviato a un invitato tramite un link pubblico univoco. L'invitato non ha bisogno di alcun account: risponde da una pagina pubblica, e la risposta appare immediatamente sulla dashboard dell'organizzatore.
Organizzatore (connesso)
└─ crea un appuntamento ──▶ appointments (status = pending, invite_token generato)
├─ «Invia via email» ──▶ funzione server sendInviteEmail ──▶ Resend
└─ «Copia link» ──▶ /invite/<token>
Invitato (senza account)
└─ apre /invite/<token> ──▶ RPC get_invite(_token) (lettura pubblica filtrata)
└─ Accetta / Rifiuta ──▶ RPC respond_to_invite(_token, _accept, _message)
└─ status = accepted | declined, responded_at = now()
Organizzatore
└─ dashboard: badge di stato, data di risposta, messaggio dell'invitato, statistiche2. Contenuto dell'email di invito
L'email viene composta lato server nella funzione sendInviteEmail e inviata tramite Resend. Mittente: valore di RESEND_FROM (predefinito «Cadence <onboarding@resend.dev>»). Oggetto: «Appuntamento: {{title}}».
Struttura del corpo, in ordine:
- Titolo «Proposta di appuntamento».
- Saluto «Ciao {{guestName}},».
- Riquadro con messaggio personalizzato (solo organizzatori Business).
- Titolo dell'appuntamento in grassetto, poi data e orario formattati per la lingua.
- Luogo e descrizione, mostrati solo se compilati.
- Pulsante d'azione verde «Conferma o rifiuta» che punta al link di invito.
- Promemoria del link in testo semplice, per i client di posta che bloccano i pulsanti.
- Blocco «Recensioni Google» (Business): nome dell'attività, valutazione, link recensioni, link Maps.
Tutti i valori dinamici vengono sottoposti a escape HTML prima dell'inserimento: nessun contenuto digitato dall'organizzatore può iniettare markup nell'email.
Variabili disponibili
| Variabile | Origine | Utilizzo |
|---|---|---|
| {{guestName}} | contacts.name | Riga di saluto «Ciao {{guestName}},». Vuota se nessun contatto è associato. |
| {{title}} | appointments.title | Oggetto dell'email «Appuntamento: {{title}}» e titolo in grassetto nel corpo. |
| {{dateRange}} | appointments.starts_at / ends_at | Formattato per lingua: «martedì 12 agosto, 15:00 → 15:30» (Intl.DateTimeFormat). |
| {{location}} | appointments.location | Riga «Luogo: …». L'intero blocco viene omesso se il campo è vuoto. |
| {{description}} | appointments.description | Paragrafo libero mostrato sotto la data. Omesso se vuoto. |
| {{inviteLink}} | origin + /invite/ + appointments.invite_token | Pulsante «Conferma o rifiuta» + promemoria del link in chiaro in fondo all'email (copia/incolla). |
| {{inviteMessage}} | profiles.invite_message (Business) | Riquadro verde con messaggio personalizzato, mostrato solo per un organizzatore Business. |
| {{businessName}} / {{googleRating}} / {{googleReviewUrl}} / {{googleMapsUrl}} | profiles.* (Business) | Blocco «Recensioni Google» a piè di email e sulla pagina di invito, se l'opzione è attiva. |
Casi in cui l'invio non va a buon fine
- no_email : il contatto non ha un indirizzo email. L'organizzatore deve copiare il link e inviarlo tramite un altro canale.
- no_api_key : la chiave di invio non è configurata. Il link di invito viene comunque restituito all'interfaccia per la copia manuale.
- provider_error : rifiuto da parte del provider email (indirizzo non valido, dominio non verificato…). Lo stato dell'appuntamento resta invariato.
- In caso di successo, invite_sent_at viene marcato con data e ora: questo campo alimenta il tracciamento «link inviati» nelle statistiche.
3. Schermate di conferma (pagina /invite/<token>)
Pagina pubblica, senza autenticazione, resa a partire dalla funzione protetta get_invite, che restituisce solo i campi necessari alla visualizzazione.
- Caricamento : scheletro animato (calendario) finché la risposta non arriva.
- Link non valido o scaduto : messaggio di errore; non viene rivelata alcuna informazione sull'organizzatore né sull'appuntamento.
- Stato «in attesa» : oggetto, data e orario, durata, luogo, nome dell'organizzatore, messaggio personalizzato (Business), campo libero facoltativo (500 caratteri max), poi due pulsanti «Accetto» e «Rifiuto».
- Dopo l'accettazione : badge verde «Accettato», conferma a schermo, promemoria delle informazioni pratiche e blocco recensioni Google se l'organizzatore è Business.
- Dopo il rifiuto : badge rosso «Rifiutato», messaggio che indica che l'organizzatore è stato avvisato; l'invitato può riaprire il link e cambiare la risposta finché l'appuntamento non è annullato.
- Appuntamento annullato : i pulsanti di risposta non hanno più effetto, lo stato mostrato resta «Annullato».
Il messaggio libero inserito dall'invitato viene troncato a 500 caratteri lato client e a 1000 lato database, poi mostrato all'organizzatore sulla scheda dell'appuntamento.
4. Stati degli appuntamenti
| Stato | Etichetta | Innesco | Effetto |
|---|---|---|---|
| pending | In attesa | Alla creazione dell'appuntamento (valore predefinito nel database). | Il link è attivo, l'invitato può rispondere. Conteggiato in «risposte attese». |
| accepted | Accettato | L'invitato clicca su «Accetto» — chiamata a respond_to_invite(_accept = true). | responded_at e response_message vengono compilati; badge verde sulla dashboard. |
| declined | Rifiutato | L'invitato clicca su «Rifiuto» — respond_to_invite(_accept = false). | Stessa registrazione dell'accettazione, badge rosso. Lo slot resta visibile per un sollecito. |
| cancelled | Annullato | Azione dell'organizzatore dalla propria dashboard. | Il link non accetta più risposte (respond_to_invite aggiorna solo gli altri stati). |
5. Flusso lato server
- Creazione / modifica / eliminazione : eseguite dal client autenticato; l'isolamento dei dati è garantito dalle regole di accesso del database.
- sendInviteEmail: funzione server protetta. Ricarica l'appuntamento filtrandolo sull'identificativo del richiedente, rifiuta se l'appuntamento non gli appartiene, compone l'email, chiama Resend e poi marca l'invio con data e ora.
- get_invite(_token): funzione del database con privilegi elevati, unica via di lettura pubblica. Restituisce solo i campi di visualizzazione e nasconde le informazioni di branding per un organizzatore non Business.
- respond_to_invite(_token, _accept, _message): aggiorna lo stato, il messaggio e la marca temporale della risposta. Agisce solo sulla riga corrispondente al token e ignora gli appuntamenti annullati.
- get_plan_usage(): restituisce il piano, il consumo del mese e l'indicatore amministratore per la visualizzazione delle quote.
6. Logica di autorizzazione
- Organizzatore : accesso completo in lettura e scrittura ai propri contatti e appuntamenti, e a essi soltanto.
- Invitato non connesso : nessun accesso diretto alle tabelle. Può solo leggere un appuntamento tramite il proprio token e rispondervi. Il token è casuale (36 caratteri esadecimali) e non dà accesso a nient'altro.
- Ruoli : memorizzati in una tabella dedicata (utente, moderatore, amministratore) e verificati lato server; non vengono mai letti dal browser per concedere un diritto.
- Amministratore : accesso all'area /admin (tutti gli account, contatti e appuntamenti) ed esenzione dalle quote del piano Free-mium.
- Piano : il cambio di livello non può essere forzato dal client; solo il processo di pagamento Stripe o un amministratore può modificarlo.
- Quote : applicate nel database prima di ogni creazione — 10/15 (Free-mium), 30/50 (Basic), 50/100 (Standard), 250/500 (Economic) contatti/appuntamenti per mese solare, illimitato su Business e per gli amministratori.
7. Checklist per il team editoriale
- Modificare un testo email: agire sul template HTML della funzione di invio, non sulla pagina di invito.
- Modificare una schermata di conferma: agire sulla pagina pubblica di invito.
- Ogni nuova variabile deve provenire da un campo già esposto dalla funzione di lettura pubblica.
- Non mostrare mai sulla pagina pubblica dati di un altro appuntamento o di un altro contatto.
- Testare sempre i tre stati: in attesa, accettato, rifiutato.