Salon de Chat
IMPORTANT : Avant de faire quoi que ce soit, tu DOIS lire BASE_SKILL.md dans le répertoire de cette skill. Il contient des conseils essentiels sur le débogage, la gestion des erreurs, la gestion d'état, le déploiement et la configuration du projet. Ces règles et patterns s'appliquent à tous les travaux RivetKit. Tout ce qui suit suppose que tu as déjà lu et compris ce document.
Exemples Fonctionnels
Si tu as besoin d'une implémentation de référence, lis le code brut de l'exemple fonctionnant dans ces templates :
Patterns pour construire un backend de salon de chat avec RivetKit : acteurs scoped par salon, historique de messages persistant, et livraison en temps réel sur des connexions WebSocket.
Code de Démarrage
Commence par l'exemple fonctionnant sur GitHub et adapte-le. Le backend est un seul acteur chatRoom ; le frontend est une app React utilisant @rivetkit/react (vois le quickstart React).
| Sujet | Résumé |
|---|---|
| Modèle de salon | Un acteur chatRoom par clé de salon. Le frontend utilise par défaut la clé general ; taper un nom de salon différent se connecte à un acteur différent. |
| Historique | Table SQLite messages créée dans db({ onMigrate }), relue avec ORDER BY id ASC. |
| Livraison | sendMessage insère la ligne, puis diffuse un événement typé newMessage à chaque client connecté. |
| Identité | Aucune dans l'exemple. sender est un simple argument d'action ; la production devrait lier l'identité à la connexion. |
Modèle Une Room Par Acteur
Chaque salon est une instance d'Acteur Rivet, adressée par clé. Le client appelle useActor({ name: "chatRoom", key: [roomId] }), qui crée ou récupère l'acteur pour ce salon. Cela te donne :
- Isolation : l'historique et les connexions de chaque salon sont complètement scopés à sa clé. Changer l'entrée du salon reconfigure le hook et se connecte à un acteur différent avec un historique séparé.
- Un seul writer sérialisé : tous les appels
sendMessagepour un salon passent par un seul acteur, donc l'ordre des messages est cohérent sans verrous. L'id SQLiteAUTOINCREMENTest l'ordre canonique, c'est pourquoigetHistorytrie paridplutôt que par timestamp. - Scaling naturel : les salons se distribuent indépendamment sur le cluster. Un salon actif ne ralentit pas les autres salons.
Stockage de l'Historique des Messages
Cet exemple stocke l'historique dans la base de données SQLite de l'acteur, pas dans l'état JSON. Choisis en fonction de la taille de l'historique et des besoins de requête :
| Approche | À Utiliser Quand | Conseils d'Implémentation |
|---|---|---|
| SQLite (ce qu'utilise cet exemple) | Historique grand ou longue durée qui a besoin de tri, limites, pagination ou recherche | Crée la table messages dans db({ onMigrate }), insère avec des requêtes paramétrées (c.db.execute("INSERT ... VALUES (?, ?, ?)", ...)), et lis avec ORDER BY id ASC. L'historique survit au sommeil de l'acteur et passe à l'échelle au-delà de ce que tu veux en mémoire. |
| État JSON | Petit historique récent, par exemple les 50 à 100 derniers messages | Ajoute à un array messages dans l'état de l'acteur et réduis à une limite à chaque envoi. Option la plus simple, mais tout l'historique vit en mémoire et il n'y a pas de couche de requête, donc cela ne convient qu'aux cas d'usage limités d'historique récent. |
Livraison par Broadcast
Les nouveaux messages atteignent les clients connectés via un événement typé :
- L'acteur déclare
events: { newMessage: event() }, oùMessageest{ sender, text, timestamp }. - L'action
sendMessageconstruit le message avec un timestamp côté serveurDate.now(), l'insère dans la tablemessages, puis appellec.broadcast("newMessage", message)et retourne le message à l'appelant. - Chaque client s'abonne avec
useEvent("newMessage", ...)et ajoute à sa liste locale. L'expéditeur affiche son propre message via le même chemin de broadcast que tout le monde, donc tous les clients restent sur un seul code path. - Le chargement de l'historique est gate de la connexion : une fois que la connexion est prête, le client appelle
getHistory()une fois pour afficher le backlog, puis s'appuie sur les événements pour tout ce qui vient après.
Utilise c.broadcast(...) pour les messages au niveau du salon. Pour les payloads privés ou par destinataire (comme les DMs dans un salon), envoie sur la connexion individuelle à la place, ce qui est une extension recommandée au-delà de cet exemple.
Indicateurs de Saisie et Présence (Extension)
L'exemple n'implémente pas les indicateurs de saisie, la présence, ou la gestion des jointures/départs de quelque sorte. Il n'y a pas de createConnState, onConnect, ou onDisconnect dans le code. Si tu en as besoin, ajoute-les comme comportement de connexion éphémère :
- Garde-le éphémère : stocke le nom d'utilisateur et le flag de saisie dans l'état per-connexion, jamais dans SQLite ou l'état d'acteur persisté. La présence est dérivée des connexions vivantes et devrait disparaître avec elles.
- Broadcast au changement uniquement : émet un événement de saisie quand un utilisateur commence ou arrête de taper, et un événement de présence depuis
onConnect/onDisconnect, plutôt que de faire du polling ou du ticking. - Expire côté client : efface un indicateur de saisie après un court timeout côté client pour qu'une connexion abandonnée ne laisse jamais une ligne "is typing" bloquée.
Boîte Aux Lettres Par Utilisateur (Extension)
Pour la livraison hors ligne, les DMs, les compteurs non lus, ou la fanout de notifications, ajoute un acteur userInbox[userId] par utilisateur. C'est une extension au-delà de l'exemple :
- L'acteur du salon fait suivre chaque message à l'acteur de la boîte aux lettres de chaque membre via des appels acteur-à-acteur, ainsi les utilisateurs qui ne sont pas connectés au salon accumulent toujours les messages.
- L'acteur de la boîte aux lettres possède l'état non lu per-utilisateur et le sert quand l'utilisateur se connecte, indépendamment des salons dans lesquels il se trouve.
- Les DMs deviennent une salle dégénérée : soit une
chatRoomclée par la paire triée de user ids, soit une livraison directe boîte-à-boîte si tu ne veux pas de sémantique d'historique partagé.
Acteurs
- Clé :
chatRoom[roomId] - Responsabilité : Possède un salon de chat. Persiste l'historique des messages du salon dans sa base de données SQLite et diffuse chaque nouveau message à chaque client connecté.
- Actions
sendMessagegetHistory
- Files d'attente
- Aucune
- Événements
newMessage
- État
- SQLite
- Table
messages:id(clé primaire autoincrement),sender,text,timestamp
Cycle de Vie
sequenceDiagram
participant A as Client A
participant B as Client B
participant R as chatRoom
A->>R: connect with key [roomId]
Note over R: every start runs onMigrate (CREATE TABLE IF NOT EXISTS messages)
A->>R: getHistory()
R-->>A: Message[] ordered by id
B->>R: connect with key [roomId]
B->>R: getHistory()
R-->>B: Message[] ordered by id
A->>R: sendMessage(sender, text)
Note over R: INSERT row with server timestamp
R-->>A: newMessage (broadcast)
R-->>B: newMessage (broadcast)
A->>R: disconnect
Note over R: history stays in SQLite for the next connection
Checklist de Sécurité
L'exemple est intentionnellement minimal et saute tous les éléments suivants. Ajoute-les avant la production :
- Auth avant la jointure : n'importe quel client peut rejoindre n'importe quel salon en connaissant son nom, et
senderest une entrée arbitraire du client à chaque appel. Valide un token pendant l'auth de connexion, lie l'identité à l'état de connexion, et vérifie l'adhésion au salon avant de servir l'historique. Ne fais jamais confiance à un nom d'expéditeur passé comme argument d'action. - Clamps de longueur de message : l'exemple accepte les messages vides et n'a pas de limite de longueur. Réduis côté serveur, rejette le texte vide, et clamp à une longueur maximale.
- Rate limiting par connexion : rate limit
sendMessagepar connexion pour arrêter le spam et l'amplification de broadcast. - Timestamps et ids côté serveur : l'exemple fait déjà cela correctement.
timestampprovient deDate.now()à l'intérieur de l'action etidde SQLiteAUTOINCREMENT. Garde-le ainsi ; n'accepte jamais les timestamps ou ids fournis par le client. - History caps :
getHistoryretourne chaque ligne sans limite. Ajoute unLIMITplus la pagination, et élagage ou archive les anciennes lignes pour qu'un salon longue durée ne puisse pas croître indéfiniment. - Requêtes paramétrées : l'exemple insère déjà avec des placeholders
?. Garde tout le texte fourni par l'utilisateur hors de l'interpolation de string SQL.
Reference Map
Actors
- Access Control
- Actions
- Actor Keys
- Actor Scheduling
- Actor Statuses
- AI and User-Generated Rivet Actors
- Authentication
- Communicating Between Actors
- Connections
- Custom Inspector Tabs
- Debugging
- Design Patterns
- Destroying Actors
- Errors
- Fetch and WebSocket Handler
- Helper Types
- Icons & Names
- Input Parameters
- Lifecycle
- Limits
- Low-Level HTTP Request Handler
- Low-Level KV Storage
- Low-Level WebSocket Handler
- Metadata
- Next.js Quickstart
- Node.js & Bun Quickstart
- Queues & Run Loops
- React Quickstart
- Realtime
- Rust Quickstart (Preview)
- Sandbox Actor
- Scaling & Concurrency
- Sharing and Joining State
- SQLite
- SQLite + Drizzle
- State & Storage
- Testing
- Troubleshooting
- Types
- Vanilla HTTP API
- Versions & Upgrades
- Workflows
Agent Os
- Agent-to-Agent Communication
- agentOS vs Sandbox
- Authentication
- Benchmarks
- Configuration
- Core Package
- Cron Jobs
- Deployment
- Embedded LLM Gateway
- Events
- Filesystem
- Limitations
- LLM Credentials
- Multiplayer
- Networking & Previews
- Overview
- Permissions
- Persistence & Sleep
- Pi
- Processes & Shell
- Queues
- Quickstart
- Sandbox Mounting
- Security & Auth
- Security Model
- Sessions
- Software
- SQLite
- System Prompt
- Tools
- Webhooks
- Workflow Automation
Clients
Connect
- Deploy To Amazon Web Services Lambda
- Deploying to AWS ECS
- Deploying to Cloudflare Workers
- Deploying to Freestyle
- Deploying to Google Cloud Run
- Deploying to Hetzner
- Deploying to Kubernetes
- Deploying to Railway
- Deploying to Rivet Compute
- Deploying to Supabase Functions
- Deploying to Vercel
- Deploying to VMs & Bare Metal
Cookbook
- AI Agent
- AI Agent Workspaces
- Chat Room
- Collaborative Text Editor
- Cron Jobs and Scheduled Tasks
- Database per Tenant
- Deploying Rivet in a VPC or Air-Gapped Network
- Live Cursors and Presence
- Multiplayer Game
General
- Actor Configuration
- Architecture
- Cross-Origin Resource Sharing
- Documentation for LLMs & AI
- Edge Networking
- Endpoints
- Environment Variables
- HTTP Server
- Logging
- Pool Configuration
- Production Checklist
- Registry Configuration
- Runtime Modes