Geeek.org • Blog Geek & High Tech 100% Indépendant

Quand j'ai finalement décidé d'activer le mode membre sur Ghost pour la newsletter, une question s'est posée assez naturellement : comment synchroniser automatiquement les abonnements Ghost avec les rôles sur le serveur Discord Geeek ?

La solution évidente, c'est de passer par du NoCode : Make, Zapier et consorts. Mais la logique d'appairage à mettre en place n'est pas forcément triviale, même en LowCode. Il faut gérer plusieurs cas : l'ajout d'un membre, sa montée en gamme vers un abonnement payant, son downgrade, sa suppression... Et surtout, il faut faire le lien entre une adresse email Ghost et un compte Discord, ce qui n'est pas directement exposé par ces outils.

J'ai donc décidé de coder moi-même le maillon manquant.

L'architecture en deux mots

Le projet s'appelle ghost-discord-worker et repose sur trois briques : Ghost, un Cloudflare Worker, et l'API Discord.

architecture.svg

Le principe est simple : Ghost envoie un webhook au Worker Cloudflare à chaque événement membre (ajout, modification, suppression). Le Worker consulte un KV store Cloudflare, qui contient une table d'appairage bidirectionnelle email ↔ discord_user_id, et met à jour les rôles Discord en conséquence. Chaque email ne peut être lié qu'à un seul compte Discord, et inversement.

Le cas le plus intéressant, c'est quand un utilisateur n'est pas encore connu du Worker. Plutôt que de demander naïvement son email sur Discord (ce qui n'apporte aucune preuve de propriété), j'ai opté pour un flux basé sur JWT. L'utilisateur connecté à son compte Ghost se rend sur une page dédiée du site, qui récupère son JWT signé via /members/api/session et l'envoie au Worker. Celui-ci vérifie la signature contre les JWKS publiques de Ghost, puis renvoie un code à 8 caractères à usage unique, valable 10 minutes. L'utilisateur tape ensuite /link <code> sur Discord et le Worker enregistre l'appairage avant d'attribuer les bons rôles immédiatement.

Les événements gérés

Le Worker couvre l'ensemble du cycle de vie d'un membre Ghost :

  • Membre ajouté (free) → rôle Member
  • Membre ajouté (paid/comped) → rôles Member + Premium Member
  • Membre supprimé → suppression de tous les rôles
  • Mise à jour free → paid → ajout du rôle Premium Member
  • Mise à jour paid → free → suppression du rôle Premium Member

Installation en 8 étapes

Étape 1 : Cloner le dépôt et installer les dépendances

git clone https://github.com/ltoinel/ghost-discord-worker
cd ghost-discord-worker
npm install

Étape 2 : Créer le namespace KV

npx wrangler kv namespace create GHOST_DISCORD_MAPPING

Récupérez l'id retourné et remplacez REPLACE_WITH_KV_NAMESPACE_ID dans le fichier wrangler.toml.

Étape 3 : Configurer les secrets Cloudflare

npx wrangler secret put WEBHOOK_SECRET
npx wrangler secret put ADMIN_SECRET
npx wrangler secret put DISCORD_BOT_TOKEN
npx wrangler secret put DISCORD_GUILD_ID
npx wrangler secret put DISCORD_PUBLIC_KEY
npx wrangler secret put DISCORD_ROLE_MEMBER
npx wrangler secret put DISCORD_ROLE_PREMIUM
npx wrangler secret put GHOST_URL
npx wrangler secret put GHOST_ADMIN_API_KEY

Le WEBHOOK_SECRET authentifie les webhooks Ghost et l'ADMIN_SECRET protège les endpoints d'administration /link via un Bearer token.

Étape 4 : Déployer le Worker

npm run deploy

Étape 5 : Configurer les webhooks dans Ghost

Dans Ghost Admin → Settings → Integrations → Custom Integration, créer trois webhooks distincts :

Événement URL
Member added https://<worker>.workers.dev/webhook/added
Member updated https://<worker>.workers.dev/webhook/updated
Member deleted https://<worker>.workers.dev/webhook/deleted

Étape 6 : Enregistrer les slash commands Discord

Enregistrez les commandes /link et /unlink via l'API Discord et configurez l'Interactions Endpoint URL vers https://<worker>.workers.dev/discord dans le Discord Developer Portal.

Étape 7 : Vérifier la hiérarchie des rôles Discord

Important : le rôle du bot doit être positionné au-dessus des rôles Member et Premium Member dans la hiérarchie de votre serveur Discord. Sans ça, le bot ne pourra pas attribuer les rôles.

Étape 8 : Créer la page d'appairage côté Ghost

Les membres ont besoin d'un moyen simple pour récupérer leur code d'appairage. Créez une page Ghost (par exemple https://<votre-site>/discord/) et collez le snippet HTML + CSS + JS prêt à l'emploi fourni dans spec/08-configuration.md dans une HTML card. Ce snippet appelle /members/api/session pour obtenir le JWT signé du membre connecté, le poste sur /code du Worker, et affiche le code à 8 caractères avec un bouton Copier ainsi qu'un compte à rebours jusqu'à expiration.

Côté membre, le parcours devient très simple : se connecter à son compte Ghost, visiter la page « Discord access », cliquer sur Get my Discord code, puis taper /link <code> sur Discord. Les rôles sont attribués instantanément.

Étape 8.a (recommandée) : reverse proxy nginx pour /code

Si votre site Ghost est derrière nginx, ajoutez un bloc location = /code afin que le navigateur appelle https://<votre-site>/code plutôt que directement https://<worker>.workers.dev/code. L'intérêt est triple : le hostname du Worker reste invisible dans les traces réseau, l'appel devient same-origin (plus de préflight CORS), et vous gardez la main sur les timeouts en bordure. Le snippet nginx complet (avec le pattern resolver + variable dans proxy_pass requis pour les IP dynamiques de workers.dev) est dans spec/08-configuration.md.

Si vous faites sans le proxy, il faut changer CODE_URL dans le JS du widget vers l'URL absolue du Worker et vérifier que le secret GHOST_URL correspond exactement à l'origine de votre site (utilisé comme Access-Control-Allow-Origin).

Un mot sur la sécurité

J'ai pris soin d'implémenter quelques bonnes pratiques : les webhooks Ghost sont authentifiés via un secret partagé en comparaison à temps constant, les interactions Discord sont vérifiées par signature Ed25519, et les endpoints d'administration /link sont protégés par un Bearer token (lui aussi vérifié en temps constant). Surtout, l'appairage repose sur une vraie preuve de propriété : le code à usage unique n'est délivré qu'après vérification du JWT du membre contre les JWKS publiques de Ghost. Les emails sont également validés au format RFC 5322 avant tout traitement pour éviter les injections, et les erreurs Discord ne sont jamais exposées côté utilisateur.

Testez la fonctionnalité

Si vous êtes membre du blog, vous pouvez tester l'appairage Ghost ↔ Discord dès maintenant ! Rendez-vous sur geeek.org/discord/, récupérez votre code en un clic, et tapez /link <code> sur le serveur Discord Geeek pour récupérer automatiquement votre rôle de membre. Si vous êtes abonné Premium, le rôle correspondant sera ajouté en même temps.

J'attends vos retours avec impatience : si quelque chose coince, vous savez où me trouver.

Conclusion

Le projet est disponible sur GitHub : ltoinel/ghost-discord-worker. Le code est en TypeScript, tourne entièrement sur l'infrastructure Cloudflare (Worker + KV), et ne nécessite donc aucun serveur à maintenir.

J'espère que cet article vous sera utile si vous gérez vous aussi une communauté sur Ghost et Discord. N'hésitez pas à venir en discuter sur le serveur Discord Geeek !


Vous êtes correctement abonné à Geeek.org
Bienvenue ! Vous êtes correctement connecté.
Parfait ! Vous êtes correctement inscrit.
Votre lien a expiré
Vérifiez vos emails et utiliser le lien magique pour vous connecter à ce site