Reciprok Docs
Infrastructure

Intégrations externes

Email, WhatsApp, transcription audio, notifications push

Reciprok est multi-canal. Cette page liste les intégrations externes nécessaires et les choix techniques pour chacune.

Vue d'ensemble

CanalUse casesProvider retenu
Email sortantCatalogue, factures, requalif, dispoResend + react-email (templates Dusty Bloom)
Email entrantCreation de demande, reponsesWebhook Resend Inbound
PaiementCheckout commissions, auto-mark paidStripe (Checkout Sessions + webhook)
WhatsAppJumelage emails, vocalAPI Meta directe (Cloud API)
CartographieCarte membres 3D, catalogue public, hub demandeMapbox GL JS (style Standard)
Audio (transcription)Voice messages, audios membresWhisper API (OpenAI)
Audio (generation TTS)Lecture des messages AIOpenAI TTS (V2)
Push notificationsAlertes desktop/PWAWeb Push API natif (VAPID)
StoragePhotos, audios, fichiersCloudflare R2 (S3-compatible) / RustFS (dev)
Logs HTTPAccess logs, acces, erreursLogixlysia → Loki (voir operations/monitoring)

Email sortant

Provider

Resend (cf. ADR-05).

Justification résumée :

  • API REST minimale, SDK TypeScript first-class
  • Webhook inbound (reciprok.com → POST JSON)
  • Tracking ouvert/cliqué natif
  • Compatible react-email (templates en JSX)
  • Bonne délivrabilité (SPF/DKIM auto-configurés)
  • Plan free 3 000 mails/mois suffit largement pour le démarrage

Architecture

Tracking

Pour chaque email envoyé, on stocke un email_message avec :

  • status : pendingsentdeliveredopenedclickedbounced / failed
  • providerId : ID renvoyé par Resend pour matcher avec les webhooks
  • openedAt, clickedAt, bouncedAt

Templates react-email

8 templates dans apps/server/src/emails/, tous basees sur base-layout.tsx (style Dusty Bloom dark : fond #1e1c22, card arrondie #2a2730, accents #c8a87e).

TemplateFlowContenu
base-layout.tsxCommunHeader RECIPROK, card dark arrondie, footer
commission-invoice.tsxFacturerMontant, taux, details membre + bouton Stripe
catalog-sent.tsxEnvoyer catalogueListe membres recommandes/compatibles + CTA
generic-message.tsxReply inbox, IAAuto-wrap tout texte plain en template
requalification.tsxRequalifierChamps manquants, message perso + CTA formulaire
availability-request.tsxDemande dispoDates cards, participants + CTA reponse
winner-selected.tsxGagnantBanniere succes verte, details evenement
not-selected.tsxNon retenuRemerciement, encouragement, "on continue"
member-onboarding.tsxOnboardingBienvenue, 3 etapes setup + lien dashboard

Auto-wrap : tout email plain (reply inbox, message IA) passe par communicationsService.sendEmail() qui detecte si le HTML est un document complet ou un fragment. Les fragments sont automatiquement wrapes dans GenericMessageEmail.

Preview : GET /api/email-preview/:template rend chaque template avec des donnees de demo. Liste sur GET /api/email-preview.

Dev : RESEND_DEV_TO redirige tous les emails vers l'owner du compte Resend (restriction plan free onboarding@resend.dev).

Threading

Tous les emails liés à une demande sont groupés dans un email_thread. Le thread est créé à la première communication. Chaque mail a un In-Reply-To et References pour que les clients mail (Gmail, Outlook) groupent correctement les réponses.

Email entrant

Stratégie

Utiliser le webhook entrant du provider (Resend permet de configurer un domaine pour recevoir des emails et POST le contenu en webhook JSON).

Endpoint : POST /api/webhooks/email/inbound

Le payload contient :

  • from, to, subject, text, html
  • Headers complets (pour le threading)
  • Pièces jointes

Routing

L'email entrant peut être :

  1. Une nouvelle demande (envoyé à demandes@reciprok.com)

    • L'IA parse le contenu, extrait les champs, crée une Request en statut qualifying (besoin de validation humaine)
    • Crée ou retrouve l'Organizer par email
    • Log un request_created event
  2. Une réponse à un thread existant

    • Identifié par le In-Reply-To ou par un token dans le subject
    • Ajouté comme email_message dans le thread
    • Log un email_received event
    • Si le thread est une requalification, l'IA tente d'extraire les nouveaux champs
  3. Inconnu

    • Envoyé dans une "inbox" non triée que l'utilisateur peut traiter manuellement

WhatsApp

Provider

API Meta directe (WhatsApp Cloud API).

On part directement sur Meta plutôt que Twilio. Raisons :

  • Twilio facture une marge fixe par message au-dessus du tarif Meta (~+25%) sans valeur ajoutée pour notre cas
  • L'API Meta Cloud est désormais aussi simple à intégrer que Twilio (REST + webhooks)
  • Pas de SDK obligatoire, on consomme l'API REST directement
  • Le compte WhatsApp Business est déjà demandé pour Reciprok côté commercial, autant ne pas dupliquer le setup
  • Médias (audio, image) gérés nativement par l'API Meta

Setup

  • Compte Meta Business + numéro WhatsApp Business vérifié
  • App Meta avec produit "WhatsApp" ajouté → token permanent (pas le token sandbox)
  • Webhook configuré sur https://app.reciprok.com/webhooks/whatsapp (hors du préfixe /api)
  • Env : WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, WHATSAPP_VERIFY_TOKEN, WHATSAPP_API_VERSION (default v21.0)

Use cases

  • Jumelage email : quand un email part, option de jumeler avec un message WhatsApp (texte ou vocal)
  • Notifications membres : quand un membre est recommandé, notification WhatsApp (en option)
  • Réponses entrantes : un membre répond par WhatsApp à une demande de dispo → webhook → mise à jour de la demande

Messages vocaux WhatsApp

L'audio reçu via WhatsApp est téléchargé via l'API Meta (GET /{media_id}), transcrit avec Whisper, et traité comme un texte entrant.

Logs HTTP, Logixlysia

Plugin Elysia natif pour les access logs structurés (JSON). Branché dès l'instanciation de l'app, il loggue chaque requête avec level, time, method, pathname, status, duration, traceId. Les logs sortent sur stdout et sont consommés par Promtail → Loki → Grafana.

import logixlysia from 'logixlysia';

new Elysia()
  .use(logixlysia({
    config: {
      ip: true,
      customLogFormat: '{level} {method} {pathname} {status} {duration}',
    },
  }))

Voir operations/monitoring pour la chaîne complète d'observabilité.

Cartographie (Mapbox GL)

Provider

Mapbox GL JS avec le style Standard (3D buildings, terrain, POIs natifs).

Use cases

  • Page /map : carte interactive 3D de tous les membres, avec clustering GeoJSON GPU-rendered pour performance (3K+ membres)
  • Vue publique catalogue : carte des membres visibles du catalogue envoyé a l'organisateur
  • Page hub /requests/[id] : mini-carte des membres candidats dans le contexte de la demande

Architecture technique

  • mapbox-gl charge directement en client (dynamic import next/dynamic avec ssr: false)
  • Source GeoJSON + layers circle + symbol pour les clusters (pas de DOM markers = performances GPU)
  • Style Standard affiche les POIs natifs (restaurants, hotels), pertinents pour le metier
  • Env : NEXT_PUBLIC_MAPBOX_TOKEN dans packages/env/src/web.ts
  • Pas de geocoding server-side, les coordonnees lat/lng sont deja sur le membre

Composants

ComposantPathUsage
MembersMapcomponents/map/members-map.tsxCarte principale /map avec clusters
MapLoadercomponents/map/map-loader.tsxDynamic import wrapper
PublicCatalogMapcomponents/map/public-catalog-map.tsxCarte vue publique catalogue

Paiement (Stripe)

Provider

Stripe, Checkout Sessions pour le paiement des commissions.

Architecture

Flow facturation

  1. L'équipe clique "Facturer" sur une commission
  2. Le backend genere un Stripe Checkout Session avec commissionId en metadata
  3. Un email Dusty Bloom (commission-invoice.tsx) est envoye au membre avec le bouton de paiement
  4. Le membre clique, paie sur Stripe
  5. Stripe envoie checkout.session.completed sur POST /webhooks/stripe
  6. Le webhook extrait metadata.commissionId et appelle markPaid() automatiquement

Templates email

Voir la section Email sortant > Templates react-email pour la liste complete des 8 templates Dusty Bloom.

Env

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...    # from Stripe CLI or Dashboard
# Dev: stripe listen --forward-to http://localhost:3000/webhooks/stripe
RESEND_DEV_TO=your@email.com       # override recipient in dev (Resend free plan restriction)

Transcription audio (Whisper)

Provider

OpenAI Whisper API ou Deepgram.

  • OpenAI Whisper : excellent en français, simple à utiliser, payé à la minute
  • Deepgram : un peu plus rapide, support du streaming, prix similaire

Recommandation : OpenAI Whisper pour démarrer.

Use cases

  1. Audio de présentation d'un membre (long, asynchrone)
  2. Réponse vocale d'un organisateur à une requalification (court, asynchrone)
  3. Message vocal de l'utilisateur dans le chat IA (court, semi-temps réel)

Architecture

Pour les audios courts (chat IA), on peut transcrire synchrone (latence ~1-3s acceptable). Pour les longs, async avec notification quand prêt.

Notifications push

Provider

Web Push API native (pas besoin de Firebase ou autre).

  • Service worker côté frontend
  • VAPID keys côté backend
  • Subscription stockée par utilisateur
  • Push envoyé via la lib web-push

Use cases

  • Nouvelle demande créée
  • Réponse à une requalification
  • Réponse de dispo d'un membre
  • Événement passé (rappel pour clôture)
  • Alerte d'inactivité (48h sans action)

Architecture

Subscription :

Push d'une notification :

Storage

Provider

S3-compatible : Cloudflare R2 (gratuit jusqu'à 10 Go), Backblaze B2 (très bon prix), ou AWS S3 standard.

Recommandation : Cloudflare R2 pour démarrer (pas de frais de sortie, gratuit en dev).

Buckets

BucketContenuPublic ?
member-photosPhotos des membresPublic (CDN)
member-audiosAudios de présentationPrivé (URL signée)
transcriptsTranscriptions JSONPrivé
voice-messagesAudios de chat IAPrivé
email-attachmentsPJ des emailsPrivé

Génération d'URL signées

Pour les contenus privés, on génère des URL signées valides 1h pour l'affichage côté frontend. L'app n'expose jamais les URLs S3 directes.

Cron jobs et workers

Plusieurs tâches doivent tourner en background :

TâcheFréquence
Re-embed les membres modifiésToutes les 5 min
Détecter les demandes inactives (48h)Toutes les heures
Détecter les événements passés non clôturésToutes les heures
Cleanup des tokens magic expirésTous les jours
Envoi des emails de relance programmésToutes les 15 min

Implémentation

Deux options :

  1. Bun + setInterval dans le serveur, simple mais lié au cycle de vie du serveur
  2. Worker dédié (process séparé) qui consomme une queue

Recommandation : commencer avec un worker Bun simple qui tourne en parallèle du serveur (process séparé via Dokploy), avec une queue Postgres LISTEN/NOTIFY pour les jobs ad hoc.

Variables d'environnement

# Database
DATABASE_URL=

# Better Auth
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
CORS_ORIGIN=

# Anthropic
ANTHROPIC_API_KEY=

# OpenAI (Whisper + embeddings)
OPENAI_API_KEY=

# Email (Resend example)
RESEND_API_KEY=
EMAIL_FROM=demandes@reciprok.com
EMAIL_INBOUND_DOMAIN=in.reciprok.com
EMAIL_WEBHOOK_SECRET=

# WhatsApp (Meta Cloud API)
META_WHATSAPP_TOKEN=
META_WHATSAPP_PHONE_ID=
META_WHATSAPP_VERIFY_TOKEN=

# Storage (R2 example)
S3_ENDPOINT=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET_PHOTOS=
S3_BUCKET_AUDIOS=

# Push notifications
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:hello@reciprok.com

À gérer via packages/env/ avec @t3-oss/env-core (déjà en place).

On this page