Kalya Pro
La première application dédiée aux approches non médicamenteuses (INM) scientifiquement validées.
📋 Table des matières
- Présentation du projet
- Architecture globale
- Stack technique
- Le SSO — Authentification centralisée & applications internes
- L'API — Le cœur métier
- L'Application Frontend
- La recherche full-text avec Meilisearch
- Système de traduction multi-langue
- Dark Mode & Design System
- Modèle économique & gestion des abonnements
- Déploiement mobile avec Capacitor
- Observabilité & Analytics
1. 📌 Présentation du projet
Le concept
Kalya Pro est un projet sur lequel je travaille depuis 4 ans. C'est la première application dédiée aux approches non médicamenteuses (INM) scientifiquement validées. L'objectif : répertorier, évaluer et restituer en temps réel les connaissances scientifiques disponibles sur des approches comme :
- 🌿 La phytothérapie et l'aromathérapie
- 💊 La micronutrition et les compléments alimentaires
- 🧘 Les approches corps-esprit et thérapies manuelles
- 🏃 L'activité physique adaptée
- 🧠 Les thérapies brèves et interventions psychosociales
L'efficacité de chaque intervention est évaluée via un Kalya Score propriétaire, un algorithme que j'ai implémenté en collaboration avec une équipe de recherche universitaire.
Un projet qui a mûri sur 4 ans
Ce projet m'a accompagné à travers plusieurs générations technologiques. Le codebase a traversé des migrations majeures au fil des ans :
| Période | Frontend | Backend | Événement clé |
|---|---|---|---|
| 2022 | Angular 14 | Laravel 9 | Première version de l’application, i18n natif |
| 2023 | Angular 15 → 16 | Laravel 9 | Intégration de Capacitor pour le mobile, recherche Meilisearch, migration vers un nouveau repo |
| 2024 | Angular 17 → 18 | Laravel 10 | Refactoring majeur : nouveau repo API, création du SSO, ajout de Stripe, refonte de l’architecture i18n (translation tables) |
| 2025–26 | Angular 18 | Laravel 10 | Stabilisation, optimisations de performance |
Chaque montée de version a été l'occasion de refactoriser en profondeur — passer de ngModules à une architecture plus modulaire, migrer les tokens d'authentification, réadapter les pipelines de build mobile, etc. Ce cycle continu de refactoring m'a appris à concevoir du code qui anticipe le changement.
Les cibles
| Audience | Accès | Usage |
|---|---|---|
| Grand public | Freemium | Découverte des INM, veille scientifique |
| Professionnels de santé | Premium | Évaluations détaillées, bibliographie, outils de prescription |
| Partenaires (marques) | B2B | Référencement produits, marketplace |
Le site commercial — kalyapro.fr

J'ai conçu le site vitrine avec un design professionnel, un dégradé teal/bleu marine qui renforce la crédibilité scientifique du projet. Le hero section présente des mockups de l'app, des témoignages de professionnels de santé, et un CTA vers une démonstration Calendly.
L'application en production — app.kalyapro.com

Pour l'interface applicative, j'ai fait le choix d'un thème sombre professionnel — un choix délibéré pour un outil "expert" qui réduit la fatigue visuelle lors de longues sessions de recherche. La sidebar de navigation, la barre de recherche proéminente et le dashboard de contenu recommandé structurent l'expérience utilisateur.
2. 🏗️ Architecture globale

Dès le départ, j'ai fait le choix d'une architecture en 3 briques indépendantes, chacune dans son propre repository Git. Cette séparation, que j'ai mise en place dès la refactorisation de 2023, a été l'une de mes meilleures décisions architecturales :
graph TB
subgraph "Client"
APP["📱 kalya-pro-app<br><b>Angular 18 + Capacitor 6</b><br>v4.4.3"]
end
subgraph "Backend"
API["⚙️ kalya-pro-api<br><b>Laravel 10</b><br>v4.3.0"]
SSO["🔐 kalya_pro_sso<br><b>Laravel 10 + Passport</b><br>v1.1.11"]
end
subgraph "Applications internes"
ADMIN["🖥️ Apps de gestion<br><b>Intranet Kalya</b>"]
end
subgraph "Services tiers"
MS["🔍 Meilisearch"]
STRIPE["💳 Stripe"]
PUSHER["📡 Pusher"]
SENTRY["🛡️ Sentry"]
MATOMO["📊 Matomo"]
end
APP -->|"REST API + JWT"| API
APP -->|"OAuth 2.0"| SSO
ADMIN -->|"OAuth 2.0"| SSO
ADMIN -->|"REST API"| API
API -->|"Search"| MS
API -->|"Payments"| STRIPE
API -->|"WebSocket"| PUSHER
APP -->|"Error tracking"| SENTRY
APP -->|"Analytics"| MATOMO
SSO -->|"Social Login"| GOOGLE["Google"]
SSO -->|"Social Login"| APPLE["Apple"]
SSO -->|"Social Login"| MICROSOFT["Microsoft"]
Pourquoi j'ai choisi cette séparation
| Critère | Avantage concret |
|---|---|
| Indépendance | Chaque brique évolue à son rythme — le SSO est en v1.1.11, l'API en v4.3.0 |
| Réutilisabilité | Le SSO sert à la fois l'app publique et les applications intranet de gestion des données |
| Sécurité | L'isolation du SSO réduit la surface d'attaque sur les credentials |
| Maintenance | Un bug dans l'API n'impacte pas l'authentification ni les outils internes |
3. 🧰 Stack technique
Vue d'ensemble
graph LR
subgraph Frontend
A[Angular 18] --> B[TailwindCSS 3]
A --> C[Capacitor 6]
A --> D[Transloco i18n]
A --> E[RxJS]
C --> F[Android]
C --> G[iOS]
C --> H[Web/PWA]
end
subgraph Backend
I[Laravel 10] --> J[PHP 8.1+]
I --> K[Eloquent ORM]
I --> L[Laravel Scout]
I --> M[Laravel Cashier]
I --> N[Laravel Socialite]
end
subgraph Services
O[MySQL]
P[Meilisearch]
Q[Stripe]
R[Redis/Cache]
end
I --> O
L --> P
M --> Q
I --> R
Après 4 ans d'itérations, j'ai convergé vers une stack que je maîtrise parfaitement et qui a prouvé sa résilience face aux montées de version et aux pivots fonctionnels.
Détail des dépendances clés
Frontend — kalya-pro-app
| Catégorie | Technologie | Pourquoi ce choix |
|---|---|---|
| Framework | Angular 18.1 | Robustesse, typage fort, architecture modulaire mature |
| Styling | TailwindCSS 3.4 | Productivité + dark mode natif |
| Mobile | Capacitor 6.x | Un seul codebase pour 3 plateformes |
| i18n | Transloco 7.4 | Plus flexible qu'Angular i18n, lazy loading des traductions |
| Paiement | ngx-stripe 18.1 | Intégration Stripe côté client, conforme PCI |
| Recherche | Meilisearch JS | Instant search avec tolérance aux fautes de frappe |
| Icônes | FontAwesome Pro 6.2 | Pack complet (duotone, thin, etc.) |
| Markdown | ngx-markdown 18.1 | Rendu du contenu scientifique riche |
| Analytics | ngx-matomo-client | Tracking respect RGPD |
| Monitoring | Sentry Angular 8 | Error tracking en production |
| Real-time | Laravel Echo + Pusher | WebSocket pour notifications |
Backend — kalya-pro-api
| Catégorie | Technologie | Pourquoi ce choix |
|---|---|---|
| Framework | Laravel 10.10 | Écosystème complet, ORM puissant |
| Traductions | astrotomic/laravel-translatable | Pattern translation-table éprouvé |
| Recherche | Laravel Scout + Meilisearch | Full-text search performante et simple |
| Paiement | Laravel Cashier | Abstraction complète de Stripe |
| Auth social | Laravel Socialite | Multi-providers en quelques lignes |
| Relations | eloquent-has-many-deep | Navigation de relations complexes |
| Qualité | Laravel Pint | Formatage cohérent du code |
SSO — kalya_pro_sso
| Catégorie | Technologie | Pourquoi ce choix |
|---|---|---|
| OAuth | Laravel Passport 11.9 | Serveur OAuth 2.0 complet, multi-clients |
| Frontend | Inertia.js + Vue | Interface de connexion SSR, rapide |
| Auth social | Socialite (Google, Apple, Microsoft) | Login social multi-providers |
| Géolocation | torann/geoip | Détection automatique du pays |
| UI | Laravel Jetstream 4 | Scaffolding auth robuste |
4. 🔐 Le SSO — Authentification centralisée & applications internes
Mon choix architectural
Très tôt dans le projet, j'ai décidé d'externaliser l'authentification dans un service dédié (kalya_pro_sso). Ce serveur OAuth 2.0 basé sur Laravel Passport ne se contente pas d'authentifier les utilisateurs de l'app publique — il sert aussi de point d'entrée unique pour les applications internes de gestion des données Kalya Pro (backoffice, outils de modération, interfaces d'administration).
Cette mutualisation signifie qu'un éditeur ou un administrateur se connecte une seule fois pour accéder à l'ensemble des outils — qu'il s'agisse de l'app publique, du backoffice de contenu, ou des outils d'analyse.
Flux d'authentification
sequenceDiagram
participant User as 👤 Utilisateur
participant App as 📱 Angular App / 🖥️ Intranet
participant SSO as 🔐 SSO Server
participant API as ⚙️ API Server
participant Social as 🌐 Social Provider
User->>App: Ouvre l'application
App->>SSO: Redirect OAuth (authRedirect)
alt Login Social
SSO->>Social: OAuth Request (Google/Apple/MS)
Social-->>SSO: Authorization Code
else Login Email
User->>SSO: Email + Password
end
SSO-->>App: Code d'autorisation (redirect callback)
App->>API: POST /auth/callback {code}
API->>SSO: Vérifie le code + échange token
SSO-->>API: Access Token + Refresh Token
API-->>App: JWT Token (access + refresh)
loop Requêtes authentifiées
App->>API: Request + Bearer Token
API-->>App: Response
end
Note over App,API: Token expiré ?
App->>API: POST /auth/refresh {refresh_token}
API-->>App: Nouveau JWT
Modèles de données du SSO
J'ai structuré le SSO autour de 10 modèles indépendants de l'API métier :
| Modèle | Rôle |
|---|---|
User |
Compte utilisateur central, partagé entre toutes les apps |
UserSetting |
Préférences (langue, dark mode, etc.) |
UserSocial |
Liens avec les comptes sociaux |
Provider |
Providers OAuth enregistrés |
Client |
Clients OAuth — chaque application (publique ou interne) est un client |
Session |
Sessions actives |
Role / Groupe |
RBAC groupé pour gérer les droits admin/éditeur/utilisateur |
AuthPartenaire |
Authentification via partenaires B2B (ex: ADNR) |
MatomoUser |
Liaison analytics |
5. ⚙️ L'API — Le cœur métier
Ma modélisation du domaine
Le domaine métier de Kalya Pro est riche et complexe — c'est d'ailleurs ce qui m'a le plus challengé techniquement. J'ai conçu une architecture avec 78 modèles Eloquent répartis en plusieurs domaines fonctionnels, articulés autour de la relation centrale "un problème de santé est évalué par des interventions non médicamenteuses" :
erDiagram
HealthProblem ||--o{ HealthProblemNpi : "évalué par"
Npi ||--o{ HealthProblemNpi : "évalue"
HealthProblemNpi ||--o{ HealthProblemNpiOutcome : "a des résultats"
HealthProblemNpi ||--o{ HealthProblemNpiPublication : "basé sur"
Publication ||--o{ HealthProblemNpiPublication : "référence"
HealthProblem ||--o{ HealthProblemCateg : "catégorisé dans"
Npi ||--o{ NpiCateg : "catégorisé dans"
HealthProblem }o--o{ MedProfil : "concerne"
HealthProblem }o--o{ Symptom : "a comme symptôme"
Npi }o--o{ MedProfil : "applicable par"
HealthProblem }o--o{ Feed : "suivi via"
Feed }o--o{ Post : "contient"
User }o--o{ Feed : "s'abonne à"
User ||--o{ Subscription : "souscrit"
Product }o--o{ Supplier : "vendu par"
Product }o--o{ FeatureProduct : "a comme caractéristique"
HealthProblem {
int id PK
string slug
string img
json actives_scores
boolean exhaustive
}
Npi {
int id PK
string slug
string img
}
HealthProblemNpi {
int id PK
float kalya_score
float biblio_count
}
Pattern: le modèle HealthProblem
Le modèle HealthProblem illustre bien les patterns que j'ai mis en place au fil des refactors :
class HealthProblem extends Model implements TranslatableContract, SearchableContract
{
use HasFactory, Translatable, HidesBasicsAttributes,
HasRelationships, HasIdOrSlugScope, Searchable;
// Attributs traduits automatiquement selon la locale
public $translatedAttributes = ['name', 'determinant', 'infos', 'description'];
protected $casts = [
'actives_scores' => 'json', // Scores sérialisés en JSON
'exhaustive' => 'boolean',
'img' => AssetUrlCast::class.':health_problem', // Cast custom pour l'URL
];
// Relations profondes (model HasManyDeep)
public function posts()
{
return $this->hasManyDeepFromRelations(
$this->feeds(),
(new Feed)->posts()
);
}
// Calcul dynamique des scores actifs
public function calcActivateScore()
{
$scores = [];
foreach (HealthProblemNpiController::SCORES as $score) {
$scores[$score] = (
$this->evaluations()->get()
->where($score, '!==', null)
->count() > 0
);
}
return json_decode(json_encode($scores));
}
}
Ce que ce code montre de mon approche :
- 🔄 Composition par traits — 6 traits mixin combinés pour gérer traduction, recherche, visibilité, slug, relations profondes et attributs masqués. Ce design a émergé progressivement au fil des refactors.
- 🌍 Transparence multilingue — les attributs
name,infos,descriptionsont résolus automatiquement selon la locale, sans que le contrôleur n'ait à s'en soucier. - 🔢 AssetUrlCast — un cast personnalisé que j'ai développé pour transformer les chemins d'images en URLs absolues, selon l'environnement.
- 🌊 hasManyDeepFromRelations — navigation HealthProblem → Feed → Post en une seule requête, essentielle pour les performances.
Ma stratégie de cache
J'ai implémenté un système de cache à deux niveaux pour gérer des données rarement modifiées mais intensément consultées :
// Cache permanent pour les données stables (catégories, listings)
return self::cacheRememberForever('healthProblem.index', function () {
return HealthProblem::all();
});
// Cache dynamique avec clé composite pour les données filtrées
$key = 'multi_' . $sortType . '_' . implode('_', $hps);
if ($filters) {
sort($filters);
$key .= '_'.implode('_', $filters);
}
return self::cacheRemember($key, function () use ($hps, $sortType, $filters) {
// Requête complexe exécutée une seule fois
});
Les clés de cache sont construites dynamiquement — chaque combinaison de tri/filtres/IDs génère une clé unique, garantissant un cache granulaire mais efficace. Le sort() sur les filtres avant concaténation évite les doublons de cache pour des requêtes identiques ordonnées différemment.
Structure des routes API
J'ai conçu une API RESTful sécurisée avec un middleware d'authentification et un gating premium :
/auth/callback POST # OAuth callback
/health_problem GET # Liste des problèmes de santé
/health_problem/{hp} GET # Détail d'un HP
/health_problem/{hp}/npi GET # INM pour un HP
/health_problem/{hp}/npi/{npi} GET 🔒 PREMIUM
/health_problem/{hp}/npi/{npi}/references GET
/npi GET # Liste des INM
/npi/{npi} GET # Détail d'une INM
/npi/{npi}/safety GET 🔒 PREMIUM
/me GET # Profil utilisateur
/me/saves GET/POST/DELETE # Favoris
/me/subscribe POST/DELETE # Abonnement
/me/feed POST # Flux personnalisé
/search GET # Recherche globale
/product GET # Marketplace produits
6. 📱 L'Application Frontend
Mon architecture Angular
J'ai organisé l'application selon une architecture modulaire par domaine métier. Au fil des refactors et des montées de version Angular (14 → 18), cette structure a prouvé sa solidité :
src/app/
├── pages/ # Navigation par fonctionnalité
│ ├── content/ # ── Le cœur de l'app ──
│ │ ├── home/ # Dashboard principal
│ │ ├── health-problem/# Fiches problèmes de santé
│ │ ├── hp-npi/ # Évaluations HP ↔ NPI
│ │ ├── npi/ # Fiches interventions
│ │ ├── guided-search/ # Recherche guidée
│ │ ├── search/ # Recherche libre
│ │ ├── news/ # Actualités scientifiques
│ │ ├── post/ # Articles
│ │ ├── publication/ # Publications scientifiques
│ │ ├── product/ # Marketplace
│ │ ├── favorites/ # Éléments sauvegardés
│ │ ├── settings/ # Paramètres
│ │ ├── faq/ # FAQ
│ │ └── contact/ # Contact
│ ├── login/ # ── Authentification ──
│ │ ├── login-form/ # Formulaire de login
│ │ ├── register/ # Inscription
│ │ ├── subscribe/ # Souscription premium
│ │ ├── carousel/ # Onboarding
│ │ ├── forgotpass/ # Mot de passe oublié
│ │ └── resetpass/ # Reset password
│ ├── loading/ # Page de chargement
│ └── complete-profil/ # Complétion du profil
│
└── shared/ # Éléments réutilisables
├── service/ # 13 services injectables
├── model/ # 35 interfaces TypeScript
├── guard/ # 3 guards (Auth, Premium, HpNpi)
├── pipe/ # Pipes personnalisés
├── directive/ # Directives
└── layout/ # Composants UI
├── base/ # Prefix kp-* (boutons, nav...)
├── card/ # Cards métier spécialisées
├── kalyaPro/ # Composants domaine Kalya
├── misc/ # Alert, footer, utilitaires
└── postPage/ # Composants articles
Le Service API centralisé — Mon chef d'orchestre
L'ApiService (609 lignes) est le point névralgique que j'ai conçu pour orchestrer toute la communication de l'app :
@Injectable({ providedIn: 'root' })
export class ApiService {
private token: any;
public user: User | undefined;
public inited = false;
public initedEvent = new Subject<boolean>();
private destroy$ = new Subject<void>();
// Cache de validation du token (optimisation performance)
private tokenValidityCache: { isValid: boolean; expires: number } | null = null;
constructor(
private http: HttpClient,
private router: Router,
private translocoService: TranslocoService,
private onlineStatusService: OnlineStatusService,
private zone: NgZone,
private matomoTracker: MatomoTracker,
) {
this.init();
}
Mon pattern d'initialisation parallélisée :
Un des refactors dont je suis le plus fier. L'initialisation séquentielle originale bloquait l'UI pendant 3-5 secondes. J'ai redesigné le flow avec Promise.allSettled pour paralléliser les opérations :
private async initializeAppParallel(): Promise<void> {
const operations: Promise<any>[] = [];
// Auth (critique)
if (this.urlParams.has('code')) {
operations.push(this.requestToken());
} else {
const jsonToken = localStorage.getItem('auth_token');
if (jsonToken) {
operations.push(this.renewToken());
}
}
// Opérations non-critiques en parallèle
operations.push(Promise.resolve(this.logApp()));
operations.push(Promise.resolve(this.initWs()));
// Exécution parallèle !
const results = await Promise.allSettled(operations);
// Vérification ciblée de l'auth uniquement
if (results[0].status === 'rejected') {
this.authRedirect();
return;
}
// Background tasks (non-bloquant)
this.updateUserCountryBackground();
}
Mon système de requêtes HTTP avec headers enrichis :
Chaque requête envoyée à l'API embarque automatiquement des métadonnées de contexte :
public async requestApi(action: string, method: string = 'GET', ...): Promise<any> {
// Validation token avant chaque requête (avec cache 30s)
if (!action.includes('/auth/refresh')) {
await this.validityToken();
}
// Headers automatiques pour tracking côté serveur
const headers = {
'X-Route': pageData['name'], // Route Angular courante
'X-Resource-Type': pageData['resource_type'],
'X-Resource-Id': resource_id(),
'X-Url': window.location.pathname,
'X-Platform-Origin': Capacitor.getPlatform(), // web/ios/android
};
// → L'API sait exactement d'où vient chaque requête
}
Mon système de Guards
J'utilise 3 guards qui s'empilent comme des couches de validation :
graph TD
R[Route demandée] --> AG{AuthGuard}
AG -->|Non connecté| SSO[Redirect SSO]
AG -->|Connecté| PG{PremiumGuard}
PG -->|Pas premium| UP[Upsell Page]
PG -->|Premium OK| HNG{HpNpiGuard}
HNG -->|Validation métier| P[✅ Page accessible]
style SSO fill:#EB5C60,color:white
style UP fill:#FBAE0A,color:white
style P fill:#70B62C,color:white
7. 🔍 La recherche full-text avec Meilisearch
Mon architecture de recherche
L'un des chantiers les plus stimulants : concevoir un système de recherche unifié multi-modèles et multi-langues. J'ai d'abord expérimenté avec Algolia avant de migrer vers Meilisearch pour des raisons de coût et de souveraineté des données.
graph TB
subgraph "Frontend"
SB["🔍 Barre de recherche"]
IS["InstantSearch.js"]
end
subgraph "API"
SC["SearchController"]
SS["SearchService"]
TR["Trait Searchable"]
end
subgraph "Meilisearch"
FR["📇 fr_global<br>(index français)"]
EN["📇 en_global<br>(index anglais)"]
end
subgraph "Models Searchable"
HP["HealthProblem"]
NPI["Npi"]
POST["Post"]
PROD["Product"]
end
SB --> IS
IS --> SC
SC --> SS
SS --> FR
SS --> EN
TR --> FR
TR --> EN
HP --> TR
NPI --> TR
POST --> TR
PROD --> TR
Le trait Searchable — Mon wrapper d'indexation multi-locale
J'ai développé un trait Searchable qui étend Laravel Scout pour ajouter la gestion des locales :
trait Searchable
{
use \Laravel\Scout\Searchable {
\Laravel\Scout\Searchable::searchableAs as scoutSearchableAs;
}
public function toSearchableArray(): array
{
// Mon système de ranking par type de contenu
$typeRank = match(class_basename($this)) {
'HealthProblem' => 3, // Toujours en premier
'Npi' => 2,
default => 1
};
return array_merge([
'id' => $this->id,
'type' => class_basename($this),
'type_rank' => $typeRank,
'composite_id' => Str::snake(class_basename($this)) . '-' . $this->id,
'resource' => $this->load($this->getSearchableRelations())->toArray(),
], $this->getSearchableAttributes());
}
// Un index par langue — chaque locale a son propre index Meilisearch
public function searchableAs(): string
{
return config('scout.global_index_base_name') . app()->getLocale();
}
// Ne pas indexer si pas de traduction disponible
public function shouldBeSearchable(): bool
{
return $this->translations()
->where('locale', app()->getLocale())
->exists();
}
}
Mes choix clés :
- 📊 Ranking par type — les
HealthProblemsont toujours priorisés, car c'est le point d'entrée principal de l'utilisateur - 🌍 Un index par langue —
fr_global,en_global— chaque langue a son propre index, ce qui évite la pollution inter-langues dans les résultats - ✅ Indexation conditionnelle — un modèle n'est indexé que s'il possède une traduction dans la locale courante
Le SearchService — Requêtage unifié
class SearchService
{
public function search(
string $query,
array|string|null $type = null,
int $page = 1,
int $perPage = 15,
string $indexName = null
): LengthAwarePaginator {
if (!$indexName) {
$indexName = config('scout.global_index_base_name') . app()->getLocale();
}
$searchParams = [
'page' => $page,
'hitsPerPage' => $perPage,
'matchingStrategy' => 'all', // Tous les mots doivent matcher
];
// Filtrage flexible par type (string ou array)
if (is_array($type)) {
$types = collect($type)->map(fn($t) => "'{$t}'")->join(', ');
$searchParams['filter'] = "type IN [{$types}]";
} elseif ($type) {
$searchParams['filter'] = "type = '{$type}'";
}
$results = $index->search($query, $searchParams);
return new LengthAwarePaginator(
items: $results->getHits(),
total: $results->getTotalHits(),
perPage: $perPage,
currentPage: $page,
);
}
}
8. 🌍 Système de traduction multi-langue
Le pattern Translation Table
Pour gérer le contenu multilingue (français, anglais), j'ai adopté le package astrotomic/laravel-translatable qui sépare les données structurelles des contenus traduits :
erDiagram
health_problems ||--o{ health_problem_translations : "1:N"
npis ||--o{ npi_translations : "1:N"
health_problems {
int id PK
string img
string slug
json actives_scores
boolean exhaustive
}
health_problem_translations {
int id PK
int health_problem_id FK
string locale
string name
string determinant
text infos
text description
}
J'ai appliqué ce pattern uniformément sur tous les modèles de contenu :
| Modèle principal | Table de traduction | Champs traduits |
|---|---|---|
HealthProblem |
health_problem_translations |
name, determinant, infos, description |
Npi |
npi_translations |
name, description, description_gp |
Post |
post_translations |
title, content, excerpt |
Product |
product_translations |
name, description |
Outcome |
outcome_translations |
name |
| ... | ... | ... |
💡 Au total, 77 migrations gèrent ce schéma — environ la moitié sont des tables
*_translations. C'est un investissement initial important, mais la cohérence et la scalabilité en valent la peine.
Middleware ApplyLocal
La locale est automatiquement détectée et appliquée de façon transparente :
Requête HTTP → Header "Locale: fr" → ApplyLocal Middleware → app()->setLocale('fr')
→ Toutes les queries Eloquent
retournent le contenu FR
9. 🎨 Dark Mode & Design System
Mon design system médical sur mesure
J'ai conçu l'identité visuelle de Kalya Pro avec un focus sur la lisibilité et la réduction de la fatigue visuelle — essentiel pour un outil utilisé quotidiennement par des professionnels de santé.
Ma palette de couleurs
Couleurs principales
━━━━━━━━━━━━━━━━━━━
🟦 dark-blue #096C8C ██████████ Accent sombre
🔵 blue #009DBE ██████████ Principal
🟢 teal #54B195 ██████████ Secondaire
🟩 green #70B62C ██████████ Score élevé
🟩 dark-green #44A365 ██████████ Succès
🟡 orange #FBAE0A ██████████ Warning
🔴 red #EB5C60 ██████████ Danger
Scores Kalya (dégradé d'efficacité)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Score 0 #888888 ██ Pas de données
Score 1 #096C8C ██ Faible
Score 2 #009DBE ██ Modéré
Score 3 #54B195 ██ Bon
Score 4 #70B62C ██ Fort
Les couleurs de score forment un dégradé intentionnel du froid (faible) au chaud (fort), permettant une lecture instinctive de l'efficacité.
Mon système de thèmes Light/Dark
Le dark mode est piloté par la classe CSS dark sur le <html> :
// tailwind.config.js
module.exports = {
darkMode: "class",
theme: {
colors: {
light: {
background: "#FDFDFF",
text: "#888888",
heading: "#2A343C",
bordure: "#CCCCCC",
},
dark: {
background: "#232C33", // Fond sombre ardoise
text: "#9BA5AE",
heading: "#EAEEF2",
"card-background": "#323F4A",
input: "#3E4A54",
},
},
},
};
Composants utilitaires Tailwind custom
J'ai développé un système de génération d'alertes colorées intégré à la config Tailwind :
function genAlert(theme, colors) {
let alerts = {};
for (var i = 0; i < colors.length; i++) {
alerts[".alert-" + colors[i]] = {
padding: theme("spacing.3"),
borderRadius: theme("borderRadius.lg"),
transition: "opacity 300ms ease-in-out",
backgroundColor: theme("colors." + colors[i] + ".200"),
color: theme("colors." + colors[i] + ".900"),
"& fa-icon": {
color: theme("colors." + colors[i] + ".500"),
},
};
}
return alerts;
}
Breakpoints et responsive
Le layout est optimisé du mobile au 4K avec 7 breakpoints :
xs ─── 512px ─── Mobile large
sm ─── 640px ─── Tablette portrait
md ─── 768px ─── Tablette paysage
lg ─── 1024px ─── Desktop
xl ─── 1280px ─── Desktop large → Container: 1075px
2xl ─── 1536px ─── Wide screen → Container: 1386px
3xl ─── 1792px ─── Ultra wide
4xl ─── 2048px ─── 4K
10. 💳 Modèle économique & gestion des abonnements
Modèle Freemium/Premium
graph TB
subgraph "Freemium"
F1["🆓 Liste des problèmes de santé"]
F2["🆓 Liste des INM"]
F3["🆓 Actualités scientifiques"]
F4["🆓 Recherche basique"]
end
subgraph "Premium"
P1["💎 Évaluations détaillées HP↔NPI"]
P2["💎 Fiches sécurité/contre-indications"]
P3["💎 Bibliographie complète"]
P4["💎 Marketplace produits"]
P5["💎 Outils de prescription"]
end
style F1 fill:#54B195,color:white
style F2 fill:#54B195,color:white
style F3 fill:#54B195,color:white
style F4 fill:#54B195,color:white
style P1 fill:#009DBE,color:white
style P2 fill:#009DBE,color:white
style P3 fill:#009DBE,color:white
style P4 fill:#009DBE,color:white
style P5 fill:#009DBE,color:white
Mon implémentation technique
J'ai implémenté le gating premium à 3 niveaux pour garantir la sécurité :
1. Middleware Backend (PremiumMiddleware) — Vérification côté serveur, impossible à contourner
Route::get('{npi}', [HealthProblemNpiController::class, 'show'])
->middleware('can:isPremium');
2. Guard Frontend (PremiumGuard) — UX fluide côté client
public isPremium(): boolean {
return !!this.user?.subscription.active;
}
public notPaid(): boolean {
return this.user?.subscription.status === 'past_due' && !this.isPremium();
}
3. Intégration Stripe — Cycle de vie complet des abonnements
API Stripe
│ │
├── kalya:syncProduct ──────► Sync prix & coupons
│ │
◄── Webhook ──────────────────┤ Événement de paiement
│ │
├── StripeEventListener ─────►│
│ ├── subscription.created │
│ ├── subscription.updated │
│ ├── payment.failed │
│ └── subscription.deleted │
│ │
├── In-App Purchase ─────────►│ (via Capacitor)
│ └── cordova-plugin-purchase│
11. 📱 Déploiement mobile avec Capacitor
Mon architecture native
Capacitor 6 me permet de déployer l'application Angular sur 3 plateformes depuis un seul codebase — un gain de temps considérable sur 4 ans de développement :
// capacitor.config.ts
const config: CapacitorConfig = {
appId: "com.kalyapro.app",
appName: "Kalya Pro",
webDir: "dist/kalya-pro-web/browser",
plugins: {
SplashScreen: {
/* config native */
},
},
};
Plugins Capacitor utilisés
| Plugin | Usage |
|---|---|
@capacitor/app |
Gestion du cycle de vie et deep links |
@capacitor/browser |
Ouverture du navigateur pour OAuth |
@capacitor/splash-screen |
Écran de chargement natif |
@capacitor/status-bar |
Personnalisation de la barre de status |
@capacitor/share |
Partage natif |
@capacitor/dialog |
Dialogues natifs |
@capawesome/capacitor-app-update |
Mises à jour in-app |
cordova-plugin-purchase |
In-App Purchases |
@capacitor-community/advertising-id |
Tracking publicitaire |
Mon pipeline de build mobile
# Synchronisation automatique des versions
npm run version-sync
# → Lit package.json v4.4.3
# → Met à jour android/app/build.gradle (versionCode + versionName)
# → Met à jour ios/App/App.xcodeproj (CFBundleVersion + CFBundleShortVersionString)
# Build complet
npm run capacitor:sync
# → version-sync → ng build (production) → npx cap sync
J'ai développé le script version-sync.js pour automatiser la synchronisation des numéros de version entre le package.json et les configurations natives Android/iOS — une source d'erreurs fréquente que j'ai voulu éliminer.
Ma gestion du deep linking natif
// Écoute des deep links natifs (OAuth callback)
App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => {
this.zone.run(() => {
const slug = event.url.split(".com").pop();
this.initQueryParams(event.url.split("?")[1]);
if (this.urlParams.has("code")) {
// Fermeture du navigateur natif + échange de code
Browser.removeAllListeners();
Browser.close();
this.requestToken();
}
if (slug) {
this.router.navigateByUrl(slug);
}
});
});
12. 📊 Observabilité & Analytics
J'ai intégré 4 outils de monitoring complémentaires pour couvrir l'ensemble du cycle de vie :
graph LR
subgraph "Erreurs"
SE["🛡️ Sentry<br>Frontend + Backend"]
end
subgraph "Analytics"
MA["📊 Matomo<br>RGPD-compliant"]
GTM["📈 Google Tag Manager"]
end
subgraph "Custom"
XT["📡 X-Headers Tracking<br>Route + Resource + Platform"]
end
APP["📱 App"] --> SE
APP --> MA
APP --> GTM
APP --> XT
XT --> API["⚙️ API"]
Mon tracking custom via X-Headers
Chaque requête API embarque automatiquement des métadonnées de contexte — un système que j'ai conçu pour comprendre finement comment les utilisateurs naviguent :
X-Route: home # Page Angular courante
X-Resource-Type: health_problem # Type de ressource consultée
X-Resource-Id: 42 # ID spécifique
X-Url: /content/health-problem/42 # URL complète
X-Platform-Origin: ios # Plateforme (web/ios/android)
→ L'API peut ainsi construire des heatmaps de navigation, analyser les parcours utilisateurs et mesurer l'engagement par contenu, le tout sans cookie tiers.
Liaison Matomo-User
private linkUserMatomo() {
this.matomoTracker.getVisitorId().then((visitorId) => {
this.requestApi('/me/tracking/matomo', 'POST', { 'matomo_id': visitorId });
});
}
Le Matomo visitor ID est lié au profil utilisateur côté serveur, permettant de corréler les sessions analytics avec les comptes utilisateurs de manière RGPD-compliant.
Chiffres clés du projet
| Métrique | Valeur |
|---|---|
| Durée de développement | 4 ans |
| Repositories | 3 (App, API, SSO) |
| Version courante | v4.4.3 |
| Modèles Eloquent | 78 |
| Migrations DB | 77 |
| Services Angular | 13 |
| Modèles TypeScript | 35 |
| Controllers API | 18 |
| Dépendances frontend | 37 |
| Plateformes cibles | 3 (Web, iOS, Android) |
| Langues supportées | 2 (FR, EN) |