L'onboarding est souvent le point de friction le plus sous-estimé des SaaS B2B. Le client signe, puis... il attend un email de bienvenue. Puis un autre email avec les identifiants. Puis un appel de setup. Puis la formation. Trois semaines plus tard, il utilise peut-être 20% des features.
Voici comment j'ai restructuré l'onboarding d'un client SaaS en un système automatisé qui guide chaque nouveau client jusqu'à sa première valeur.
Le problème à résoudre
Avant : 4 personnes passaient 2h chacune par nouveau client pour :
- Créer le compte en base de données
- Envoyer les accès manuellement
- Programmer les calls de formation
- Relancer si pas de connexion en J+3
- Envoyer les ressources de documentation
Coût moyen : 8h humaines par onboarding, pour 12 nouveaux clients/mois = 96h/mois soit ~2,5 jours-homme.
L'architecture du système
Signature contrat (PandaDoc/DocuSign webhook)
↓
Création compte automatique
↓
Email bienvenue + accès (Resend)
↓
J+1 : Email "Première connexion" si pas connecté
↓
J+3 : SMS/email si toujours pas de login
↓
J+5 : Alerte interne Slack si client inactif
↓
J+7 : Email "Quick win" avec cas d'usage #1
↓
J+14 : NPS survey + offre upgrade
↓
J+30 : Review d'activation (score d'usage)
Étape 1 : Déclencher sur la signature
// Webhook PandaDoc → n8n
interface PandaDocWebhook {
event: 'document_state_changed'
data: {
id: string
name: string
status: 'document.completed' // signature reçue
metadata: {
client_email: string
client_name: string
plan: 'starter' | 'pro' | 'enterprise'
}
}
}
// Dans n8n : Webhook Trigger → IF status === 'document.completed'
Étape 2 : Créer le compte automatiquement
async function createClientAccount(data: PandaDocWebhook['data']) {
const { client_email, client_name, plan } = data.metadata
// 1. Créer l'entrée Supabase
const { data: account } = await supabase
.from('accounts')
.insert({
email: client_email,
name: client_name,
plan,
onboarding_started_at: new Date().toISOString(),
})
.select()
.single()
// 2. Générer un mot de passe temporaire sécurisé
const tempPassword = generateSecurePassword()
// 3. Créer l'auth Supabase
await supabase.auth.admin.createUser({
email: client_email,
password: tempPassword,
email_confirm: true,
})
// 4. Envoyer l'email de bienvenue
await sendWelcomeEmail({
to: client_email,
name: client_name,
loginUrl: `https://app.example.com/login`,
tempPassword,
plan,
documentId: data.id,
})
return account
}
Étape 3 : Séquence email intelligente
La clé : chaque email est conditionnel à l'activité réelle du client.
// Vérification quotidienne à 9h (n8n Schedule Trigger)
async function checkOnboardingProgress() {
const clients = await supabase
.from('accounts')
.select('*, last_login, feature_usage, onboarding_step')
.gte('onboarding_started_at', subDays(new Date(), 30))
.is('churned', false)
for (const client of clients) {
const daysSinceSignup = differenceInDays(new Date(), client.onboarding_started_at)
const hasLoggedIn = client.last_login !== null
const hasUsedCoreFeature = client.feature_usage?.core_action > 0
// J+1 sans connexion
if (daysSinceSignup === 1 && !hasLoggedIn) {
await sendEmail(client.email, 'onboarding-reminder-d1')
}
// J+7 : envoyer le "quick win" même s'ils sont actifs
if (daysSinceSignup === 7) {
await sendEmail(client.email, 'quick-win-use-case', {
use_case: getRelevantUseCase(client.plan),
})
}
// J+5 : alerte interne si toujours inactif
if (daysSinceSignup === 5 && !hasLoggedIn) {
await sendSlackAlert({
channel: '#onboarding-alerts',
message: `⚠️ ${client.name} n'a pas encore ouvert l'app — J+5`,
actions: [
{ label: 'Appeler', url: `tel:${client.phone}` },
{ label: 'Voir le compte', url: `https://admin.example.com/clients/${client.id}` },
],
})
}
}
}
Templates d'emails
L'email J+1 qui convertit le mieux :
Objet : Vos accès [NomProduit] — on a tout préparé pour vous
Bonjour [Prénom],
Votre compte est prêt depuis hier. Si vous n'avez pas encore eu le temps de vous
connecter, pas d'inquiétude — vos accès sont permanents.
→ [Se connecter maintenant] ← (bouton CTA principal)
En 5 minutes, vous pouvez :
1. Connecter votre première source de données
2. Voir votre dashboard se peupler
3. Exporter votre premier rapport
Si vous préférez qu'on le fasse ensemble : [Réserver 15 min] ← (Calendly)
À disposition,
[Prénom] de l'équipe [NomProduit]
Simple, court, une action principale, une action secondaire.
Mesurer l'activation
Le vrai indicateur : pas le nombre de connexions, mais la "First Core Action".
// Événement à tracker dans votre app
analytics.track('core_action_completed', {
userId: client.id,
action: 'first_export', // ou 'first_automation', 'first_report', etc.
daysFromSignup: differenceInDays(new Date(), client.onboarding_started_at),
plan: client.plan,
})
// Dans n8n : Webhook → mettre à jour feature_usage → déclencher email "Félicitations"
Résultats après 60 jours
| Métrique | Avant | Après | | ----------------------------- | --------- | ---------- | | Temps team onboarding | 8h/client | <30 min | | Délai première connexion | 2-3 jours | <12h | | Activation (core action J+14) | 34% | 61% | | Churn J+90 | 22% | 11% |
Le coût IA et automation : ~$15/mois. Le gain humain : 96h/mois.
Un détail de sécurité qu'on a corrigé en cours de route
La première version envoyait le mot de passe temporaire en clair dans l'email de bienvenue (tempPassword dans le payload, comme montré plus haut). Ça fonctionne, mais ça laisse un mot de passe valide qui traîne dans une boîte mail — potentiellement pour toujours, si le client ne le change jamais.
On est passés à un lien magique à usage unique, qui élimine complètement le problème :
async function createClientAccount(data: PandaDocWebhook['data']) {
// ... création du compte identique ...
// Au lieu d'un mot de passe temporaire, un lien de connexion à usage unique
const { data: magicLink } = await supabase.auth.admin.generateLink({
type: 'magiclink',
email: client_email,
options: { redirectTo: 'https://app.example.com/onboarding/welcome' },
})
await sendWelcomeEmail({
to: client_email,
name: client_name,
magicLinkUrl: magicLink.properties.action_link, // expire après 1h, usage unique
plan,
})
}
Le bénéfice n'est pas que théorique : ça supprime aussi une étape de friction pour le client (pas de mot de passe à copier-coller), ce qui a fait légèrement remonter le taux de première connexion en J+1. Sur un système d'onboarding entièrement automatisé, chaque étape manuelle retirée au client compte autant que celles retirées à l'équipe.
Ce qu'on n'a pas automatisé (intentionnellement)
- L'appel de kickoff pour les clients Enterprise (>500€/mois) — le contact humain fait la différence
- La résolution de bugs spécifiques au setup — trop de variabilité
- Les upsells — une conversation humaine convertit 3x mieux
L'automatisation est au service de l'équipe, pas un remplacement. Les humains traitent les exceptions ; le système gère le flux normal.