Architecture Blueprint

Flowly Web Dashboard V1

Architecture complète, SQL, endpoints API, structure de fichiers et design system pour le dashboard SaaS — basé sur l'analyse de ton extension existante.

Fichiers core
24
composants + pages
Endpoints API
11
routes sécurisées
Tables SQL
7
avec RLS Supabase
Stack
Next.js
+ Supabase + Stripe
🔗 Compatibilité extension
Tout est construit pour être 100% compatible avec ton background.js existant. Le magic link /api/session-sync/generate est déjà présent dans ton code — il suffit d'adapter la destination vers le dashboard web.
01 — Structure des fichiers

Arborescence du projet

Architecture Next.js App Router propre, scalable, organisée par domaine métier.

Frontend / App

flowly-dashboard/ ├── app/ │ ├── (auth)/ │ │ ├── login/page.tsx │ │ └── callback/page.tsx ← magic link │ ├── (dashboard)/ │ │ ├── layout.tsx ← sidebar + auth guard │ │ ├── overview/page.tsx │ │ ├── history/page.tsx │ │ ├── analytics/page.tsx │ │ └── settings/page.tsx │ ├── api/ │ │ ├── track/route.ts ← même endpoint extension │ │ ├── overview/route.ts │ │ ├── history/route.ts │ │ ├── streak/route.ts │ │ ├── subscription-check/route.ts │ │ ├── session-sync/ │ │ │ └── generate/route.ts ← déjà dans background.js │ │ └── billing/route.ts │ └── layout.tsx ├── components/ │ ├── ui/ ← primitives │ │ ├── Card.tsx │ │ ├── Badge.tsx │ │ ├── Button.tsx │ │ └── Skeleton.tsx │ ├── charts/ │ │ ├── DonutChart.tsx │ │ ├── BarChart.tsx │ │ └── ActivityCalendar.tsx │ ├── dashboard/ │ │ ├── StreakWidget.tsx ← nouveau (design amélioré) │ │ ├── StatsOverview.tsx │ │ ├── TopDomains.tsx │ │ ├── FocusScore.tsx │ │ └── PaywallGate.tsx │ └── layout/ │ ├── Sidebar.tsx │ └── Header.tsx

Backend / Config

├── lib/ │ ├── supabase-server.ts ← server-side client │ ├── supabase-client.ts ← browser client │ ├── auth.ts ← helpers session │ ├── plans.ts ← limites free/pro │ └── queries/ │ ├── overview.ts │ ├── history.ts │ └── streak.ts ├── types/ │ ├── database.ts ← types Supabase générés │ └── api.ts ├── middleware.ts ← auth guard global ├── next.config.ts ├── .env.local── Desktop Tracker (dossier séparé) flowly-desktop/ ├── package.json ├── src/ │ ├── main.ts ← Electron main process │ ├── tracker.ts ← active window detection │ ├── sync.ts ← flush vers Supabase │ └── auth.ts ← magic link login └── electron-builder.yml
02 — Base de données

Schéma SQL complet

7 tables. Toutes avec RLS activé. Compatible avec les données existantes de l'extension.

Tables existantes (à conserver)

TableDescriptionUtilisée par
tracking_dataTemps par domaine, par jour, par userExtension + Dashboard
user_subscriptionsPlan actif, status, datesExtension + Dashboard

Nouvelles tables à créer

-- ══════════════════════════════════════════════════════════ -- FLOWLY V1 — SQL Migration -- Compatible Supabase + RLS -- ══════════════════════════════════════════════════════════ -- 1. Streak utilisateur (remplace localStorage) CREATE TABLE user_streaks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users ON DELETE CASCADE UNIQUE NOT NULL, streak_current INT DEFAULT 0, streak_best INT DEFAULT 0, last_visit DATE, updated_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE user_streaks ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users own their streak" ON user_streaks FOR ALL USING (auth.uid() = user_id); -- 2. Apps bureau (Desktop Tracker) CREATE TABLE app_tracking ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL, app_name TEXT NOT NULL, duration INT NOT NULL, -- secondes tracked_date DATE NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE app_tracking ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users own their app data" ON app_tracking FOR ALL USING (auth.uid() = user_id); -- 3. Record personnel (record de temps max) CREATE TABLE user_stats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users ON DELETE CASCADE UNIQUE NOT NULL, record_time_sec INT DEFAULT 0, record_time_date DATE, total_tracked_sec BIGINT DEFAULT 0, updated_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE user_stats ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users own their stats" ON user_stats FOR ALL USING (auth.uid() = user_id); -- 4. Index de performance CREATE INDEX ON tracking_data (user_id, tracked_date DESC); CREATE INDEX ON app_tracking (user_id, tracked_date DESC); -- 5. Vue agrégée (dashboard overview) — lecture rapide CREATE OR REPLACE VIEW v_daily_totals AS SELECT user_id, tracked_date, SUM(duration) AS total_sec, COUNT DISTINCT(domain) AS domain_count FROM tracking_data GROUP BY user_id, tracked_date; -- Sécurité vue ALTER VIEW v_daily_totals OWNER TO authenticated;
⚠️ Note sur tracking_data
Ton extension utilise déjà une table pour les données de tracking via /api/track. Vérifie que la structure de ta table actuelle correspond à : user_id, domain, duration (int, secondes), tracked_date (date). Si ta colonne de date s'appelle différemment, adapter les queries.
03 — Auth & Session

Connexion extension → dashboard

Le flow magic link est déjà implémenté dans ton background.js. Il suffit de créer la page de réception côté web.

Flow existant (background.js)

// Déjà dans ton background.js ✓ if (msg.action === "OPEN_BILLING") { const resp = await fetch( `${FLOWLY_API_BASE}/api/session-sync/generate`, { method: "POST", body: JSON.stringify({ access_token: session.access_token, refresh_token: session.refresh_token }) } ); // → ouvre l'onglet avec redirect_url await chrome.tabs.create({ url: json.redirect_url }); }

Page callback à créer

// app/(auth)/callback/page.tsx export default async function CallbackPage({ searchParams }) { const token = searchParams.token; const supabase = createClient(); // Valider le token one-time depuis ta table const { data } = await supabase .from('session_tokens') .select('access_token, refresh_token') .eq('token', token) .gt('expires_at', 'now()') .single(); await supabase.auth.setSession({ access_token: data.access_token, refresh_token: data.refresh_token, }); redirect('/overview'); }

middleware.ts — Protection globale

// middleware.ts — garde toutes les routes /dashboard/* export async function middleware(request: NextRequest) { const supabase = createMiddlewareClient({ req: request, res }); const { data: { session } } = await supabase.auth.getSession(); if (!session && request.nextUrl.pathname.startsWith('/overview')) { return NextResponse.redirect(new URL('/login', request.url)); } } export const config = { matcher: ['/overview/:path*', '/history/:path*', '/analytics/:path*'] };
04 — Endpoints API

Routes API sécurisées

Tous les endpoints vérifient le JWT Supabase en entrée. Les limites Free/Pro sont appliquées côté serveur.

POST/api/trackReçoit les données de l'extension (déjà existant)
GET/api/overview?date=YYYY-MM-DDDonnées vue générale du jour
GET/api/history?from=&to=&limit=Historique filtré (limité si Free)
GET/api/analytics/weekStats 7 jours — Pro uniquement
GET/api/analytics/trendsTendances mois/comparaisons — Pro uniquement
GET/api/streakRécupère le streak actuel
POST/api/streakMet à jour le streak (upsert)
POST/api/subscription-checkVérifie le plan (même logique extension)
POST/api/session-sync/generateMagic link extension→web (déjà existant)
POST/api/billingPortail Stripe
DELETE/api/delete-accountSuppression compte (déjà existant)

Pattern sécurité — chaque endpoint

// Patron répété sur tous les endpoints export async function GET(req: Request) { // 1. Auth const token = req.headers.get('authorization')?.replace('Bearer ', ''); const { data: { user }, error } = await supabase.auth.getUser(token); if (!user || error) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); // 2. Plan check (Free vs Pro) const plan = await getUserPlan(user.id); // lit user_subscriptions const limits = PLAN_LIMITS[plan]; // lib/plans.ts // 3. Appliquer les limites avant la query const maxDays = limits.historyDays; // 3 (free) ou Infinity (pro) const fromDate = new Date(); fromDate.setDate(fromDate.getDate() - maxDays); // 4. Query Supabase avec contrainte const { data } = await supabase .from('tracking_data') .select() .eq('user_id', user.id) // RLS en plus .gte('tracked_date', fromDate); return NextResponse.json({ data, plan }); }

lib/plans.ts — Limites centralisées

export const PLAN_LIMITS = { free: { historyDays: 3, weekAnalytics: false, trends: false, exportCsv: false, desktopTracker: false, aiInsights: false, }, pro: { historyDays: Infinity, weekAnalytics: true, trends: true, exportCsv: true, desktopTracker: true, aiInsights: true, } } as const;
05 — Vue générale

Dashboard Overview

Page principale avec stats du jour, top domaines, score de focus et activité hebdomadaire.

Flowly Dashboard
Overview
History
Analytics
Settings
● Connected
AD
Aujourd'hui — Lun 9 juin
Bonjour, Adam 👋
Streak 7 🔥
Temps total
4h 32m
↓ 38min vs hier
Score de focus
76
↑ +4 pts
Sites visités
12
3 productifs
Streak actuel
7 jours
Record: 14 j
Top domaines
github.com
1h 24m
notion.so
52m
linear.app
38m
youtube.com
24m
Répartition
Travail58%
Social21%
Distracteur21%

Composant StatsOverview.tsx

// components/dashboard/StatsOverview.tsx interface OverviewData { totalSec: number; prevTotalSec: number; topDomains: { domain: string; total_sec: number }[]; focusScore: number; domainCount: number; } export function StatsOverview({ data, plan }: { data: OverviewData; plan: 'free' | 'pro' }) { const delta = data.totalSec - data.prevTotalSec; return ( <div className="grid grid-cols-4 gap-3"> <StatCard label="Temps total" value={formatDuration(data.totalSec)} delta={delta} /> <StatCard label="Focus score" value={data.focusScore} highlight /> <StatCard label="Sites visités" value={data.domainCount} /> <StreakWidget /> {"// voir section 07"} </div> ); }
06 — Historique

Page Historique

Timeline avec filtres par date. Free = 3 jours. Pro = illimité avec export CSV.

Logique de limite côté API

// app/api/history/route.ts export async function GET(req: Request) { const { user } = await getAuthUser(req); const plan = await getUserPlan(user.id); const limits = PLAN_LIMITS[plan]; const url = new URL(req.url); let from = url.searchParams.get('from'); const to = url.searchParams.get('to'); // Forcer la limite côté serveur const minDate = new Date(); minDate.setDate(minDate.getDate() - limits.historyDays); if (!from || new Date(from) < minDate) { from = minDate.toISOString().slice(0, 10); } const { data } = await supabase .from('tracking_data') .select() .eq('user_id', user.id) .gte('tracked_date', from) .lte('tracked_date', to ?? 'today') .order('tracked_date', { ascending: false }); return NextResponse.json({ data, plan, limitedTo: from }); }

Limites visibles UI

// components/dashboard/PaywallGate.tsx // Wrapper réutilisable pour le paywall export function PaywallGate({ feature, plan, children, }: { feature: keyof typeof PLAN_LIMITS.free; plan: 'free' | 'pro'; children: React.ReactNode; }) { const hasAccess = PLAN_LIMITS[plan][feature]; if (hasAccess) return children; return ( <div className="relative"> <div className="blur-sm pointer-events-none select-none"> {children} </div> <UpgradeOverlay feature={feature} /> </div> ); } // Utilisation dans history/page.tsx <PaywallGate feature="exportCsv" plan={plan}> <ExportButton /> </PaywallGate>
07 — Focus Streak

Streak redesigné pour le web

Même logique que l'extension — stockage Supabase, animations web améliorées, calendrier 7 jours visible.

7 jours

Série active · Record : 14 jours

Cette semaine
L
M
M
J
V
S
D
+1 aujourd'hui
Reviens demain !

StreakWidget.tsx

// components/dashboard/StreakWidget.tsx // Améliorations vs extension : calendrier 7j + progress bar export function StreakWidget() { const { data } = useSWR('/api/streak', fetcher); // Met à jour le streak à chaque ouverture useEffect(() => { fetch('/api/streak', { method: 'POST' }); // upsert silencieux }, []); const weekDays = getLast7Days(); // ['2025-06-03', ..., today] const activeDays = new Set(data?.activeDates ?? []); return ( <div className="streak-card"> <div className="streak-flame">{data?.count ?? 0} 🔥</div> <div className="week-grid"> {weekDays.map(day => ( <div key={day} className={activeDays.has(day) ? 'day active' : 'day'} > {dayLabel(day)} </div> ))} </div> <div className="streak-sub">Record : {data?.best ?? 0} jours</div> </div> ); }

API /api/streak (GET)

// Réponse { count: 7, best: 14, last_visit: "2025-06-09", activeDates: [ "2025-06-03", "2025-06-04", ... ] }

API /api/streak (POST) — upsert

const today = new Date().toISOString().slice(0,10); const yesterday = getYesterday(); // Même logique que dashboard.js de l'extension if (last === today) { /* rien */ } elif(last === yesterday){ count++; } else { count = 1; } best = Math.max(best, count); // upsert dans user_streaks
08 — Plans Free / Pro

Architecture de monétisation

Compatible avec la structure Stripe existante. Protections à deux niveaux : frontend + backend obligatoirement.

Free
0€ / toujours
Dashboard aujourd'hui
Historique 3 jours
Focus Streak
Historique complet
Analytics semaine/mois
Export CSV
Desktop Tracker
Pro ✦
4.99€ / mois ou 29€ lifetime
Dashboard aujourd'hui
Historique illimité
Focus Streak + Records
Analytics semaine/mois
Tendances + comparaisons
Export CSV complet
Desktop Tracker
🔐 Règle absolue — toujours vérifier côté serveur
Ne jamais faire confiance au plan passé dans le frontend. Chaque endpoint API doit relire user_subscriptions en base. Le flou CSS et les composants masqués côté client sont UX, pas sécurité.
09 — Desktop Tracker

Tracker d'applications bureau

Solution Electron légère. Détecte la fenêtre active, flush vers Supabase toutes les 15s. Même pattern que l'extension.

Electron — choix recommandé

Multiplatform (Mac/Win/Linux), accès natif aux APIs OS, même JS que l'extension. Distribution via GitHub Releases, auto-update avec electron-updater.

Sécurité

Login via magic link (même flow que l'extension → dashboard). Token stocké dans electron-store encrypté. Requêtes signées avec access_token JWT.

Même architecture de tracking

Buffer en mémoire + flush 15s. Détection AFK à 60s. Envoie vers /api/track (même endpoint que l'extension). Les données arrivent dans app_tracking.

tracker.ts — Core

// flowly-desktop/src/tracker.ts import activeWin from 'active-win'; let buffer: Record<string, number> = {}; let currentApp: string | null = null; let lastActivity = Date.now(); // Tick toutes les secondes (même logique que background.js) setInterval(async () => { const win = await activeWin.getActiveWindow(); if (!win) return; const app = win.owner.name; // "Google Chrome", "VS Code"... // Idle detection if (Date.now() - lastActivity > 60000) return; buffer[app] = (buffer[app] ?? 0) + 1; }, 1000); // Flush toutes les 15s → même endpoint que l'extension setInterval(async () => { const session = await getSession(); if (!session) return; for (const [app, duration] of Object.entries(buffer)) { await fetch(`${API_BASE}/api/track`, { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}` }, body: JSON.stringify({ app, duration, type: 'desktop' }) }); } buffer = {}; }, 15000);

Dépendances minimales

electron ^28 active-win ^8 @supabase/supabase-js ^2 electron-store ^8 electron-updater ^6 electron-builder (build)
10 — Design System

Tokens CSS & conventions

Design sobre et lisible inspiré Supabase/Linear. Pas de néons, pas d'effets superflus. Tailwind CSS recommandé.

Palette de couleurs

Background
#0a0c10
Surface
#111418
Accent (bleu)
#4F7FFF
Succès
#10B981
Streak (flamme)
#FF7A3A

Typographie

DISPLAY / HEADINGS
DM Sans 600
BODY
DM Sans 400 — texte de contenu, descriptions, labels secondaires.
MONOSPACE (temps, IDs, code)
4h 32m · DM Mono 500
LABEL / BADGE
UPPERCASE · 10px · 600 · 0.12em spacing

tailwind.config.ts — Configuration recommandée

export default { theme: { extend: { colors: { bg: '#0a0c10', surface: '#111418', surface2: '#181c22', border: 'rgba(255,255,255,0.07)', accent: '#4F7FFF', streak: '#FF7A3A', }, fontFamily: { sans: ['DM Sans', 'system-ui'], mono: ['DM Mono', 'monospace'], }, borderRadius: { card: '12px', } } } };
11 — Checklist de lancement

Ordre d'implémentation recommandé

Séquence en 4 semaines pour une V1 solide, en partant de ce qui existe déjà.

Semaine 1 — Fondations

Setup Next.js + Supabase

Auth, middleware, types générés auto depuis Supabase

Page callback magic link

Compatible avec OPEN_BILLING existant

Migration SQL

Créer les 3 nouvelles tables + indexes

lib/plans.ts + PLAN_LIMITS

Centraliser les limites dès le départ

Semaine 2 — Dashboard core

Sidebar + layout authentifié

Composant Sidebar.tsx, Header.tsx

Vue overview complète

Stats, top domaines, donut chart, streak widget

Page historique

Timeline + PaywallGate pour les 3j/illimité

Semaine 3 — Analytics Pro

BarChart semaine / mois

Charts.js ou Recharts, protégé PaywallGate

Export CSV côté API

Protégé plan.exportCsv

Page settings + billing

Portail Stripe, gestion abonnement

Semaine 4 — Desktop Tracker

Scaffolding Electron

main.ts, tracker.ts, sync.ts, auth.ts

Build + distribution

electron-builder + GitHub Releases

Intégration dashboard

Section "Apps bureau" dans overview, fusion des données

✅ Points clés à ne pas oublier
1. Ne jamais faire confiance au client pour les limites de plan — toujours vérifier en base côté API.
2. RLS Supabase activé sur toutes les tables — même si l'API vérifie, c'est une défense en profondeur.
3. Le magic link est déjà dans background.js — adapter uniquement l'URL de destination.
4. Le streak doit rester simple côté serveur — la même logique que dashboard.js de l'extension, juste portée en API.
5. Recharts ou Chart.js — ne pas réinventer les graphiques, utiliser une lib éprouvée.