Motivația

Am construit o grămadă de aplicații CRUD. REST API, bază de date, frontend React, poate niște polling sau un strat WebSocket pentru actualizări „în timp real". Setup-ul este familiar, dar repetitiv: definești o schemă, scrii resolver-e, construiești rute API, gestionezi cache-ul pe client, gestionezi logica de reconectare WebSocket, te lupți cu datele stale...

Când am descoperit Convex, propunerea era convingătoare: definești schema și funcțiile, iar fiecare useQuery devine un abonament live care se re-randează automat când datele se schimbă. Fără pub/sub, fără cod WebSocket, fără invalidare de cache. Am vrut să testez această afirmație construind ceva suficient de complex pentru a o stresa.

Proiectul: TaskFlow

TaskFlow este o aplicație colaborativă de gestionare a task-urilor, cu echipe, atribuiri de task-uri, comentarii și notificări în timp real. Stack-ul:

  • Next.js 15 cu App Router și TypeScript
  • Convex pentru baza de date, funcțiile serverless și sincronizarea în timp real
  • Convex Auth pentru autentificare (email/parolă cu GitHub ca provider)
  • Tailwind CSS cu primitive Radix UI pentru biblioteca de componente

Arhitectura Serviciilor

Aici lucrurile au devenit interesante. Am organizat backend-ul Convex ca o arhitectură de tip microservicii - nu servicii deployate separat, ci module izolate logic cu granițe clare:

  • taskService.ts - CRUD, tranziții de stare, filtrare, îmbogățire date
  • userService.ts - profiluri, validarea unicității email-ului, verificări de dependențe
  • teamService.ts - gestionarea echipelor, roluri de membri, ștergeri în cascadă
  • commentService.ts - comentarii grupate pe thread-uri per task
  • notificationService.ts - alerte pentru atribuiri, completări și comentarii
  • eventBus.ts - definiții tipizate de evenimente pentru comunicarea inter-servicii

Fiecare serviciu exportă propriile tipuri (TaskFilters, CreateTeamInput, etc.) și funcții helper private. Fișierele publice precum convex/tasks.ts re-exportă ca un facade, oferind frontend-ului o suprafață API curată precum api.tasks.getTasks.

Timp Real: Funcționează și Gata

Momentul în care am rulat useQuery(api.tasks.getTasksByTeam, {}) și am văzut lista de task-uri actualizându-se în alt tab de browser în câteva milisecunde după o mutație - fără niciun cod scris de mine - m-am convins.

Într-un stack tradițional, pentru a realiza asta ai nevoie de:

  1. Un endpoint REST sau GraphQL
  2. Un server WebSocket (Socket.IO, Pusher etc.)
  3. Gestionarea stării pe client cu invalidare de cache
  4. Logică de reconectare și retry
  5. Rezolvarea conflictelor pentru editări concurente

Convex le colapsează pe toate cinci într-o singură interogare reactivă. Hook-ul useQuery se abonează la datele subiacente; când orice mutație atinge tabelele relevante, interogarea se re-evaluează pe server și trimite noul rezultat tuturor clienților conectați.

Comunicarea Inter-Servicii: Evenimente Inline

Deoarece totul rulează pe funcțiile serverless ale Convex, nu puteam folosi RabbitMQ sau un event bus tradițional. În schimb, comunicarea inter-servicii se întâmplă direct în interiorul mutațiilor:

Când un task este atribuit cuiva, mutația taskService.create inserează o notificare inline - dacă cel căruia i s-a atribuit task-ul nu este creatorul, inserează în tabela notifications cu tipul task_assigned. useQuery-ul serviciului de notificări o preia instant. Similar, când se adaugă un comentariu, mutația verifică dacă cel căruia i-a fost atribuit task-ul diferă de autorul comentariului și creează o notificare comment_added.

Acesta este mai simplu decât un event bus propriu-zis, dar cuplează serviciile la nivelul mutațiilor. Pentru un sistem de producție la scară aș vrea evenimente async reale, dar pentru scopul acestui proiect abordarea directă funcționează curat.

Schema: Cinci Tabele, Zece Indexuri

Schema Convex definește cinci tabele cu indexuri alese cu grijă:

  • users - nume, email, rol (admin/member), indexat după email
  • teams - nume, descriere, proprietar, indexat după proprietar
  • teamMembers - tabelă de joncțiune cu rol, indexată după echipă, după utilizator și după echipă+utilizator (compus)
  • tasks - titlu, stare, prioritate, assignee, creator, echipă, dată scadentă - indexat în cinci moduri pentru diversele pattern-uri de interogare
  • comments - referință la task, autor, conținut, indexat după task și autor
  • notifications - utilizator, tip, titlu, mesaj, flag de citire - indexat după utilizator și după utilizator+citit (pentru numărul de necitite)

Indexul compus pe teamMembers(teamId, userId) este critic pentru verificarea „este deja acest utilizator membru?" din addMember. Fără el, fiecare adăugare de membru ar scana întreaga tabelă.

Reguli de Business de Care Sunt Mândru

Ștergeri în cascadă cu curățare inter-servicii: Ștergerea unei echipe elimină toți membrii săi, apoi toate task-urile sale, apoi echipa în sine. Ștergerea unui utilizator verifică mai întâi task-urile create (aruncând excepție dacă există vreunul), apoi elimină toate apartenențele la echipe. Acestea nu sunt cascade la nivel de bază de date - sunt logică aplicativă explicită, ceea ce face regulile vizibile și testabile.

Unicitatea email-ului între mutații: Helper-ul validateEmailUnique interoghează indexul by_email înainte de orice creare sau actualizare de utilizator. Dacă email-ul este deja luat (de un alt utilizator), aruncă excepție. Parametrul de excludere gestionează cazul de actualizare în care utilizatorul își păstrează propriul email.

Ce M-a Surprins

Validarea schemei la deploy time. Convex validează schema când deployezi funcțiile, nu la runtime. Acest lucru a prins mai multe bug-uri în timpul dezvoltării - tipuri de câmpuri nepotrivite, indexuri lipsă - înainte să ajungă în producție.

Niciun loading state pentru datele existente. Când interogarea este deja în cache, nu există flash de „Se încarcă..." la navigare. Datele sunt pur și simplu acolo. Doar primul render al unei interogări noi arată undefined.

Ce Aș Face Diferit

Paginare de la bun început. La început am încărcat toate task-urile cu .collect() - bine pentru 50 de task-uri, problematic pentru 5.000. Adăugarea paginării bazate pe cursor a fost simplă, dar ar fi trebuit inclusă de la start.

Actualizări optimiste. Convex le suportă, dar nu am implementat niciuna. Pentru performanța percepută pe conexiuni lente, mutarea unui task și reflectarea ei imediată în UI (înainte de confirmarea serverului) ar îmbunătăți semnificativ experiența.

Codul sursă: github.com/ionutn0301/convex-task-manager

Înapoi la Articole