On vient de livrer Hively — une app mobile iOS et Android de mise en relation entre particuliers et prestataires de services à domicile. Ménage, garde d’enfants, pet-sitting, coaching fitness, coiffure : l’utilisateur cherche un prestataire autour de chez lui, consulte son profil et ses disponibilités, réserve un créneau et échange par chat. Le prestataire gère ses réservations, son planning et ses tarifs. Le back-office supervise l’ensemble. Le projet s’appelait initialement Hively — il a été renommé Hively en cours de route, mais le concept reste le même : une ruche de prestataires de confiance.
Le projet a mobilisé quatre personnes sur six mois. Emmanuelle et Xavier au développement, Aurélien et Raphaël en encadrement technique. C’est notre premier projet d’envergure en Ionic — et le premier où on a poussé le tooling JavaScript aussi loin.
Cet article revient sur les choix techniques, ce qui a bien marché, et ce qu’on ferait différemment.
Le contexte : une marketplace dans l’ère du “Uber for X”
On est en plein boom du “Uber for X”. TaskRabbit, Thumbtack, Handy — il y a une marketplace pour chaque verticale de services, et les levées de fonds pleuvent. Le modèle est toujours le même : mise en relation instantanée, paiement intégré, notation bilatérale.
Le client arrive avec une vision claire : une plateforme collaborative centrée sur Londres, multi-verticales (ménage, garde d’enfants, fitness, pet-care, beauté), avec un système de communautés et de groupes privés pour la viralité. Ce qui distingue Hively, c’est l’angle social — on ne cherche pas un prestataire anonyme, on découvre celui que ses voisins ou ses collègues recommandent. Le budget impose une app hybride plutôt que deux développements natifs — mais le rendu doit être soigné.
On connaît Ionic depuis 2014, on avait partagé nos premières impressions à l’époque. Deux ans plus tard, le framework a mûri, la communauté a grandi, et on a assez de recul pour engager un projet de cette taille. C’est aussi notre troisième marketplace : après le backend Rails de Simone.paris (réservation de coiffure à domicile) et plusieurs projets non documentés dans le secteur, on a accumulé un savoir-faire sur les systèmes de réservation, les machines à états et la gestion des disponibilités.
L’app : ce qu’on a livré
Avant d’entrer dans la technique, un tour de l’app telle que l’utilisateur la découvre.
Le parcours d’entrée
L’identité visuelle est construite autour de l’hexagone — un clin d’œil à la ruche. Les icônes sont en line-art monochrome sur fond clair, dans l’esprit du flat design 2.0 qui domine cette année. La connexion Facebook n’est pas qu’un raccourci d’inscription — c’est le mécanisme de découverte du réseau social. Les amis de l’utilisateur qui sont sur Hively apparaissent automatiquement, et leurs prestataires favoris deviennent des recommandations.
La réservation, le chat et le réseau
Le parcours de réservation s’adapte dynamiquement au service choisi — un ménage propose des options de repassage et de matériel fourni, un personal training demande le type de session et la durée. C’est le concern Profilable côté Rails qui pilote cette flexibilité — les attributs de chaque service sont déclarés dans un fichier YAML, pas codés en dur.
La messagerie est un des gros morceaux du projet. Chaque réservation crée automatiquement une conversation entre l’utilisateur et le prestataire. Action Cable (la nouveauté phare de Rails 5) gère le temps réel — les messages apparaissent instantanément. Le FeedService côté Rails génère des messages lisibles pour chaque transition d’état dans le fil d’activité.
Ce qui distingue Hively des concurrents, c’est le levier social. L’utilisateur appartient à des communautés publiques (sa résidence, son entreprise, son club sportif) et crée des groupes privés (sa famille, ses amis proches). Les membres partagent leurs prestataires favoris et leurs avis — la confiance passe par le bouche-à-oreille numérique.
Le stack : ce qu’on a choisi et pourquoi
Côté mobile : Ionic 1 + AngularJS
On est partis sur Ionic 1.3 et AngularJS 1.x. Ionic 2 est en bêta depuis le début de l’année — on suit de près, mais une bêta n’est pas un choix de production pour un projet client de six mois. La version 1 est stable, documentée, et notre équipe la maîtrise.
AngularJS reste notre fondation. Les patterns sont éprouvés — services, factories, directives, injection de dépendances. Le routing avec ui-router et ses états imbriqués gère naturellement la navigation par onglets :
export default function TabState($stateProvider) {
$stateProvider.state('tab', {
url: '/tab',
abstract: true,
template: '<ion-tabs class="tabs-icon-top tabs-color-active-assertive">' +
' <tab-home></tab-home>' +
' <tab-bookings></tab-bookings>' +
' <tab-chat></tab-chat>' +
' <tab-account></tab-account>' +
'</ion-tabs>'
});
}
L’app s’organise autour de cinq onglets : découverte des services, réservations, messagerie, fil d’activité et compte. Chaque onglet est un module Angular autonome avec ses propres états, ses contrôleurs et ses vues.
Le pipeline JavaScript : Babel, Webpack, ES6
C’est là qu’on a fait un vrai pas en avant par rapport à nos projets précédents.
Webpack remplace Gulp. On ne l’a pas choisi par effet de mode — on l’a choisi parce que le modèle mental est meilleur. Au lieu de décrire des tâches (copier ça, compiler ça, minifier ça), on décrit un graphe de dépendances. Webpack comprend que ce fichier .js importe ce fichier .scss qui référence cette image — et il optimise l’ensemble.
Notre config Webpack gère tout le pipeline en un seul outil :
- JavaScript : transpilation Babel avec source maps
- SCSS : compilation Sass → CSS, puis PostCSS avec Autoprefixer
- Images : optimisation avec pngquant, mozjpeg et svgo en production
- Fonts : extraction des icônes Ionic dans un dossier dédié
- HTML : minification des templates Angular
En production, on sépare le code applicatif du SDK Ionic dans deux bundles distincts — le vendor bundle est caché agressivement côté client puisqu’il ne change quasiment jamais entre les releases.
Babel 6 avec babel-preset-env nous permet d’écrire du JavaScript moderne sans sacrifier la compatibilité. On cible les deux dernières versions d’iOS et Android 4+. Babel transpile exactement ce qui est nécessaire — pas plus.
Et ça change tout. On écrit du vrai ES2015 en production :
// Avant — AngularJS classique
angular.module('app').factory('User', function($http, $q, LocalCache) {
var User = function(data) {
this.id = data.id;
this.name = data.first_name + ' ' + data.last_name;
this.avatar = data.avatar_url || 'img/default-avatar.png';
};
User.cache = {};
User.findOrCreate = function(data) {
if (User.cache[data.id]) {
angular.extend(User.cache[data.id], new User(data));
return User.cache[data.id];
}
User.cache[data.id] = new User(data);
return User.cache[data.id];
};
return User;
});
// Après — ES6 avec Babel
import angular from 'angular';
import _ from 'lodash';
export default class User {
constructor(data) {
this.id = data.id;
this.name = `${data.first_name} ${data.last_name}`;
this.avatar = data.avatar_url || 'img/default-avatar.png';
}
static findOrCreate(data) {
const existing = User.cache[data.id];
if (existing) {
return _.extend(existing, new User(data));
}
User.cache[data.id] = new User(data);
return User.cache[data.id];
}
}
User.cache = {};
Les import/export remplacent les dépendances implicites par des dépendances explicites — Webpack sait exactement ce que chaque module utilise. Les template literals, les arrow functions, le destructuring, le spread operator — tout ça rend le code plus lisible sans aucun compromis sur la compatibilité. Babel fait le travail.
On a même activé transform-object-rest-spread pour le stage 3 du TC39 — le spread sur les objets est tellement pratique pour composer les options de requête qu’on a jugé le risque acceptable.
CSS : SCSS, PostCSS, Flexbox
Côté styles, on utilise SCSS comme préprocesseur — variables, mixins, nesting, partials. Chaque composant a son fichier .scss dédié. On a une centaine de fichiers de styles, organisés par module.
PostCSS avec Autoprefixer passe après Sass. On écrit du CSS standard, Autoprefixer ajoute les préfixes vendeurs nécessaires pour nos cibles. Plus de -webkit- manuels, plus d’oublis. On déclare nos cibles dans la config et on n’y pense plus.
Et surtout : on utilise Flexbox partout. 2016, c’est l’année où Flexbox est enfin utilisable en production — le support navigateur dépasse 93%, et sur les WebViews mobiles qu’on cible (iOS 8+ et Android 4.4+ basé sur Chromium), c’est solide. Fini les float et les clearfix pour aligner des éléments. Le layout des cartes de prestataires, la grille de filtres, les formulaires multi-colonnes — tout est en Flexbox.
.provider-card {
display: flex;
align-items: center;
padding: 12px 16px;
&__avatar {
flex: 0 0 56px;
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
}
&__info {
flex: 1;
margin-left: 12px;
display: flex;
flex-direction: column;
}
&__actions {
flex: 0 0 auto;
display: flex;
gap: 8px;
}
}
On suit la convention BEM pour le nommage — .block__element--modifier. Sur un projet avec autant de composants (directives Angular custom, modales, filtres, badges, cartes), une convention stricte de nommage CSS est indispensable. Sans ça, les collisions de styles deviennent ingérables dès le troisième mois.
Côté serveur : Rails 5, l’API qui tombe à pic
Rails 5 — sorti il y a quatre mois
Rails 5.0 est sorti le 30 juin. On a commencé le projet en mars sur Rails 4.2, et on a migré en juillet — la migration s’est faite en une journée. Le timing était parfait : les deux grandes nouveautés de Rails 5 répondaient exactement à nos besoins.
Le mode API. Rails 5 introduit rails new --api, un mode allégé qui supprime tout ce qui concerne le rendu HTML — les vues, l’asset pipeline, les helpers de formulaire. On garde ActiveRecord, les migrations, le routing, les contrôleurs, Sidekiq, les mailers — sans le poids d’un framework full-stack quand on ne sert que du JSON. C’est ce qu’on fait : une API REST pour l’app mobile, point.
Avant Rails 5, on devait désactiver manuellement les middlewares inutiles, retirer les générateurs de vues, ignorer l’asset pipeline. Maintenant, c’est un flag. Rails assume enfin officiellement ce que la communauté faisait depuis des années : utiliser Rails comme backend d’une app JavaScript.
Action Cable. Le framework WebSocket intégré à Rails. On l’utilise pour le chat en temps réel — les conversations entre utilisateurs et prestataires. Avant, on aurait dû brancher Faye ou un service tiers. Avec Action Cable, c’est du Ruby idiomatique, intégré au cycle de vie Rails, avec l’authentification qui suit naturellement.
class ConversationChannel < ApplicationCable::Channel
def subscribed
conversation = Conversation.find(params[:id])
reject unless conversation.participant?(current_user)
stream_for conversation
end
def speak(data)
message = conversation.messages.create!(
sender: current_user,
text: data['text']
)
ConversationChannel.broadcast_to(conversation, message.as_json)
end
end
C’est du Rails pur — pas de configuration de serveur WebSocket séparé, pas de gem tierce, pas de sérialisation manuelle. On déclare un channel, on stream, on broadcast. Le déploiement sur Heroku fonctionne out of the box avec le dyno web Puma.
Une architecture polymorphique pour le multi-services
Le défi principal côté données : une marketplace multi-verticales. Un prestataire de ménage n’a pas les mêmes attributs qu’un coach fitness ou un dog-walker. Le ménage facture à l’heure avec des options (repassage, linge, produits ménagers). Le fitness propose du yoga, du pilates ou du personal training en sessions de 30 ou 60 minutes. Le dog-walking distingue les balades solo et en groupe, par taille de chien.
On a opté pour des profils polymorphiques. Chaque service (cleaning, fitness, petcare…) a sa propre classe Profile::Cleaning, Profile::Fitness, Profile::DogWalking — avec ses attributs et ses tarifs spécifiques. Le prestataire est lié à un profil via une association polymorphique :
class Provider < ApplicationRecord
belongs_to :profile, polymorphic: true
has_many :availabilities
has_many :holidays
has_many :bookings, through: :abstract_bookings
end
class Profile::Cleaning < ApplicationRecord
include Profilable
monetize :hourly_rate_cents
monetize :brings_equipment_rate_cents
validates :hourly_rate_cents, presence: true
end
class Profile::DogWalking < ApplicationRecord
include Profilable
monetize :solo_30_rate_cents
monetize :group_30_rate_cents
# Filtres par taille de chien
scope :accepts_large, -> { where(large_dogs: true) }
end
Le concern Profilable mutualise la logique commune — l’association inverse vers le provider, la validation du tarif de base, les scopes de recherche. Chaque profil ne contient que ce qui lui est propre.
Ce design nous permet d’ajouter une nouvelle verticale sans toucher au modèle Provider ni à l’API — une migration pour la table de profil, un fichier de configuration YAML pour les attributs de filtrage, et c’est en ligne. On a ajouté le pet-sitting et la beauté en cours de projet sans casser l’existant.
La machine à états des réservations
Une réservation passe par plusieurs états : pending → confirmed / declined / cancelled → completed. On utilise AASM (Acts As State Machine) pour modéliser ces transitions :
class AbstractBooking < ApplicationRecord
include AASM
aasm column: :state do
state :pending, initial: true
state :confirmed, :declined, :cancelled, :completed
event :confirm do
transitions from: :pending, to: :confirmed
after { notify_user(:booking_confirmed) }
end
event :decline do
transitions from: :pending, to: :declined
after { notify_user(:booking_declined) }
end
event :cancel do
transitions from: [:pending, :confirmed], to: :cancelled
after { CancelBookingJob.perform_later(self) }
end
end
end
Chaque transition déclenche des effets de bord — envoi d’email via Mandrill, SMS via Nexmo, mise à jour du fil d’activité. Les jobs lourds passent par Sidekiq : rappels avant la prestation, annulation automatique si le prestataire ne confirme pas dans les délais, génération des réservations récurrentes.
Les réservations récurrentes sont un cas intéressant. Un BookingPlan (hebdomadaire, bi-hebdomadaire ou mensuel) génère automatiquement les Booking enfants via un job Sidekiq. On calcule les créneaux futurs en tenant compte des disponibilités du prestataire, de ses jours de congé et de ses réservations existantes — la gem time_round (développée en interne) gère la composition de plages horaires.
Disponibilités et recherche
Le moteur de recherche est le cœur de l’expérience utilisateur. L’utilisateur entre son code postal, choisit un service, une date et une durée — l’API retourne les prestataires disponibles sur ce créneau, triés par pertinence.
Côté backend, le calcul de disponibilité est plus complexe qu’il n’y paraît :
Chaque prestataire déclare ses disponibilités par jour de la semaine (lundi de 9h à 18h, mardi de 14h à 20h…). On soustrait les congés, les réservations déjà confirmées, et un buffer de 12 heures pour éviter les réservations de dernière minute. Le tout est filtré par préfixe de code postal — chaque prestataire déclare les zones qu’il couvre.
Le FilterService applique ensuite les filtres spécifiques au service : fourchette de tarif, options (le prestataire apporte-t-il son matériel ?), capacité (taille du chien acceptée, nombre d’enfants).
L’app mobile : les détails qui comptent
Les directives Angular comme composants
On a construit une bibliothèque de directives Angular réutilisables — l’équivalent de ce qu’on appellerait des composants aujourd’hui. Chaque élément d’interface a sa directive, son template et son fichier SCSS :
<address-search-field>— champ d’adresse avec autocomplétion Google Places<rating>— affichage de la note du prestataire en étoiles<status-badge>— badge coloré selon l’état de la réservation<multi-select-filter>— filtre à cases à cocher avec compteur<range-filter>— curseur de fourchette de prix<service-image>— image du service avec lazy loading<contact-provider-buttons>— boutons d’appel et de message
Chaque directive est un module Angular isolé — son propre scope, ses propres bindings. On les compose pour construire les écrans. L’écran de recherche, par exemple, assemble une <address-search-field>, une grille de <service-image>, des <multi-select-filter> et des <range-filter> — le contrôleur ne fait que câbler les données.
L’intégration Google Maps et Places
La géolocalisation est omniprésente. L’utilisateur renseigne son adresse via l’autocomplétion Google Places — on envoie le code postal au backend pour filtrer les prestataires. La carte affiche la localisation de la prestation sur une Google Map embarquée via le SDK natif Cordova.
On a dû faire un choix : le SDK JavaScript de Google Maps (dans la WebView) ou le plugin Cordova natif. On a opté pour le plugin natif — la différence de fluidité est frappante. Le pinch-to-zoom, le pan, le rendu des tuiles : tout est rendu par le moteur natif de la plateforme. La WebView affiche la carte en dessous — c’est transparent pour l’utilisateur.
L’autocomplétion d’adresse utilise l’API Google Places JS dans la WebView — c’est un champ de texte classique, pas besoin de la performance native ici. Le résultat est parsé pour extraire le code postal, qui alimente la recherche backend.
Authentification et cache
L’authentification utilise un système de tokens Bearer. À la connexion (email/mot de passe ou Facebook via Koala), le serveur génère un token de 128 caractères, stocké côté serveur en SHA-256. Le token clair est renvoyé une seule fois au client, qui le stocke localement et l’envoie dans le header Authorization de chaque requête.
class Authentication {
constructor($http, LocalCache) {
this.$http = $http;
this.cache = LocalCache;
}
signIn({ email, password }) {
return this.$http.post('/api/v1/users/login', { email, password })
.then(({ data }) => {
this.cache.set('access_token', data.access_token);
this.$http.defaults.headers.common.Authorization =
`Bearer ${data.access_token}`;
this.currentUser = new User(data.user);
return this.currentUser;
});
}
}
Le token expire au bout de trois mois — un compromis entre sécurité et confort d’utilisation sur mobile, où on ne veut pas que l’utilisateur se reconnecte toutes les semaines.
Côté cache, chaque modèle (User, Provider, Booking) maintient un cache en mémoire via Lodash. Quand l’API renvoie un provider déjà connu, on met à jour l’instance existante plutôt que d’en créer une nouvelle. Les vues qui référencent cet objet se mettent à jour automatiquement grâce au two-way binding d’AngularJS — un des rares cas où le dirty-checking d’Angular 1 est un vrai avantage.
Le back-office : ActiveAdmin au service du métier
Le back-office n’est pas un afterthought — c’est un outil critique pour l’équipe opérationnelle. ActiveAdmin génère un panneau d’administration complet à partir des modèles Rails.
On gère depuis le back-office :
- Les prestataires : inscription, vérification d’identité, validation DBS (Disclosure and Barring Service, l’équivalent britannique du casier judiciaire), vérification d’assurance
- Les réservations : suivi des états, résolution de conflits, annulations manuelles
- Les communautés : création, modération, gestion des membres
- Les services : activation/désactivation par zone géographique
- Les candidatures : intake depuis le site Webflow, traitement par l’équipe
ActiveAdmin brille sur ce type de projet. Le DSL déclaratif permet de mettre en place des filtres avancés (Ransack), des exports CSV, des actions personnalisées sur les ressources — le tout sans coder un CRUD from scratch. On a ajouté des actions custom pour les transitions AASM (confirmer, annuler une réservation depuis le back-office) et pour la gestion des disponibilités.
Le footer du back-office affiche l’environnement (staging/production) et la révision Git — indispensable quand l’équipe opérationnelle signale un bug et qu’on doit savoir quelle version tourne.
Le déploiement : Heroku et Sidekiq
L’infrastructure est simple et assumée. Heroku pour l’hébergement — un dyno web Puma, un dyno worker Sidekiq, un add-on PostgreSQL, un add-on Redis pour les queues Sidekiq et les sessions Action Cable. Le tout déployé via un script deploy maison qui pousse sur le remote Heroku et tag la révision Git.
On a trois environnements : dev, staging, production. Le staging est l’exacte réplique de la production — même stack, mêmes add-ons, mêmes variables d’environnement (sauf les clés API qui pointent vers des sandboxes). L’équipe QA valide sur staging avant chaque mise en production.
Les fichiers statiques (avatars, photos de profil, galeries) sont stockés sur Amazon S3 via CarrierWave et fog-aws. MiniMagick redimensionne les images à l’upload — on génère plusieurs tailles pour optimiser le chargement dans l’app mobile.
Les emails transactionnels passent par Mandrill — confirmation de réservation, rappels, réinitialisation de mot de passe. Les SMS critiques (nouvelle demande de réservation pour le prestataire) passent par Nexmo. En développement, un intercepteur redirige tous les emails et SMS vers l’équipe pour éviter les envois accidentels aux vrais utilisateurs.
Ce qu’on a appris
Ionic 1 tient la route pour un projet ambitieux
Six mois, cinq onglets principaux, des dizaines d’écrans, des dizaines de directives custom, un chat en temps réel, Google Maps natif, Facebook Login, des animations de transition — et l’app tourne correctement sur iOS et Android. Pas parfaitement — le scroll sur les listes longues reste un poil moins fluide que du natif, et certains appareils Android d’entrée de gamme souffrent. Mais pour un budget qui n’aurait permis qu’une seule plateforme en natif, on livre les deux.
Webpack et Babel, on ne revient pas en arrière
On ne reviendra pas en arrière. Écrire du JavaScript moderne avec des modules ES6, des classes, des arrow functions — et le transpiler automatiquement vers du code compatible — c’est un workflow qui rend le code plus lisible et plus maintenable. Webpack unifie le pipeline de build d’une manière que Gulp ne permettait pas.
On regarde de près ce qui se passe du côté de Webpack 2, qui devrait stabiliser le tree-shaking (élimination du code mort basée sur les imports ES6). Et Rollup, un bundler alternatif qui pousse cette approche encore plus loin.
Rails 5 confirme la direction
Le mode API est une validation officielle de ce qu’on faisait déjà — utiliser Rails comme backend pour des apps mobiles. Action Cable rend le temps réel accessible sans infrastructure supplémentaire. La commande unifiée rails simplifie le quotidien. Et l’écosystème — Devise, Sidekiq, ActiveAdmin, AASM, CarrierWave — reste le plus productif qu’on connaisse pour ce type de projet.
On est passés de Rails 4.2 à 5.0 en cours de projet, et la migration a pris une journée. Pas de surprise, pas de breaking change majeur — juste des gains. C’est ce qu’on attend d’un framework mature.
La prochaine étape
On garde un œil attentif sur Ionic 2, en Release Candidate depuis quelques semaines. La réécriture complète sur Angular 2 et TypeScript promet des gains de performance significatifs et un outillage plus moderne. Pour notre prochain projet hybride, on évaluera sérieusement la migration.
Côté backend, Rails 5.1 se profile — avec l’intégration native de Webpack (via le gem Webpacker) et le support de Yarn pour la gestion des dépendances JavaScript. Rails embrasse enfin l’écosystème JavaScript frontend au lieu de le combattre. C’est un signal fort.
Et React Native continue sa montée en puissance. Facebook, Instagram et Airbnb l’utilisent en production. On le teste en interne depuis quelques mois — le modèle “composants natifs rendus depuis JavaScript” est séduisant. Mais pour l’instant, l’écosystème est encore jeune, et Ionic reste notre choix par défaut pour les projets hybrides.
Ce qu’on retient : on peut livrer une app mobile ambitieuse avec des technologies web. Mais il faut choisir les bons outils, les pousser, et ne pas faire de compromis sur la qualité du code.
Lire aussi : Webpack, Babel et ES6 dans une app Ionic — le détail du pipeline JavaScript par Aurélien, et Rails 5 API : modéliser les disponibilités — l’architecture backend par Raphaël.