Job·Ka
Tous les emplois des employeurs québécois. Un seul endroit.
Job·Ka collecte les offres directement sur les pages carrières des employeurs du Québec — pas sur les job boards. Chaque offre est traçable à sa source, les salaires sont normalisés en $/h et $/an, et la candidature se fait chez l'employeur. Zéro boîte noire, zéro intermédiaire.
Sommaire
- Pourquoi Job·Ka ?
- L'architecture en 30 secondes
- Le pipeline en détail
- Structure du dépôt
- Démarrage rapide
- CLI
run.py - Variables d'environnement
- API
- Ajouter un employeur (≈ 10 lignes)
- Tests
- Production
- Principes
- Écosystème Groupe KA
Pourquoi Job·Ka ?
Les grands sites d'emploi mélangent annonces payantes, doublons repostés et offres mortes. L'offre originale — celle de la page carrière de l'employeur — est la seule source fiable : à jour, complète, sans frais de mise en avant.
Job·Ka se branche directement sur les systèmes de recrutement (ATS) des employeurs : Workday, Lever, Greenhouse, SmartRecruiters, ou leur site maison. Un connecteur = un employeur. Le tout resynchronisé jour et nuit.
Ce n'est pas un job board qui vend de la visibilité, c'est un index fidèle de ce que les employeurs québécois affichent réellement — avec les salaires en clair quand ils existent (normalisés en $/h et $/an), parce que la transparence salariale est le différenciateur clé.
L'architecture en 30 secondes
flowchart LR
subgraph Sources
W["Workday"] --> C
L["Lever"] --> C
G["Greenhouse"] --> C
S["SmartRecruiters"] --> C
X["Sites maison<br/>(HTML / Scrapfly)"] --> C
end
C["Connecteurs<br/>(auto-découverts)"] --> N["finalize()<br/>normalisation"]
N --> D[("SQLite<br/>data/jobka.db")]
W2["⏱ Watcher horaire<br/>(PM2)"] -.-> C
D --> A["FastAPI<br/>/api/*"]
A --> F["React + Vite<br/>frontend/dist"]
D --> GEO["Géocodage<br/>Nominatim + Adresses Québec"]
GEO --> D
| Couche | Rôle | Fichiers |
|---|---|---|
| Connecteurs | 1 employeur = 1 module ; plateformes ATS partagées | jobka/connectors/ |
| Schéma | JobPosting + finalize() (normalisation commune) |
jobka/schema.py, jobka/normalize.py |
| Persistance | diff par content_hash, cycle de vie, anti-dérive |
jobka/db.py |
| Pipeline | ingestion, boucle watch, géocodage, dédup |
jobka/ingest.py, geocode.py, dedup.py |
| API + Web | FastAPI + frontend Vite servi en statique | jobka/web.py, frontend/ |
| Tests | fixtures HTTP enregistrées, rejeu hors ligne | jobka/fixtures.py, tests/ |
Le pipeline en détail
Connecteurs auto-découverts
Toute sous-classe de BaseConnector avec un source_id est enregistrée par
pkgutil — aucun registre à maintenir. Les plateformes ATS (Workday, Lever,
Greenhouse, SmartRecruiters) sont des classes de base : brancher un nouvel
employeur = ~10 lignes. Politesse : throttling par source (request_delay),
User-Agent identifiable JobKaBot/1.0, budgets de requêtes, cache BD des
pages détail.
Diff engine avec garde-fou anti-dérive
Chaque sync compare le content_hash de chaque offre : ajout, mise à jour, ou
disparition. Une offre absente 2 syncs consécutifs passe active=0 (jamais de
DELETE). Si une source retourne ≤ 25 % de sa médiane historique, alerte et
retraits suspendus — un connecteur cassé ne vide pas la base. Les offres
dont la date_limite est dépassée sont retirées même si encore affichées.
Salaires normalisés — jamais inventés
parse_salary() exige un signe monétaire ou une unité explicite, applique des
bornes de plausibilité par unité, et convertit en $/h et $/an (40 h × 52 sem.).
Une offre sans salaire reste sans salaire. L'historique des changements est
conservé (salary_log).
Géocodage sans invention
Nominatim (1 req/1,1 s) + repli officiel Adresses Québec (MERN, lots de 200),
cache persistant par lieu normalisé, validation par bounding box provinciale.
Sans adresse : centroïde de ville. Introuvable : geocode_failed=1, jamais de
coordonnées bidon.
Déduplication inter-sources
Blocage par clé exacte titre normalisé | employeur | ville + union-find avec
exigence d'un second signal concordant (type, salaire ±4 %, date ±7 j). Le
canonique est la source la plus autoritaire (page carrière avant agrégat) ;
les doublons sont masqués (dup_of), jamais supprimés.
Structure du dépôt
job-ka/
├── run.py # point d'entrée : sync | watch | serve | geocode | record
├── requirements.txt # fastapi, uvicorn, requests, beautifulsoup4
├── jobka/
│ ├── schema.py # dataclass JobPosting + finalize()
│ ├── normalize.py # salaires, dates, mode/type, lieux, catégories
│ ├── db.py # SQLite : diff, cycle de vie, anti-dérive
│ ├── ingest.py # run() / watch() — orchestration
│ ├── dedup.py # déduplication inter-sources (union-find)
│ ├── geocode.py # Nominatim + Adresses Québec + cache
│ ├── web.py # API FastAPI + /health + frontend statique
│ ├── fixtures.py # enregistrement/rejeu HTTP pour les tests
│ └── connectors/
│ ├── base.py # BaseConnector (throttling, Scrapfly, cache détail)
│ ├── workday.py # plateforme Workday (facette Québec auto-découverte)
│ ├── lever.py # plateforme Lever
│ ├── greenhouse.py # plateforme Greenhouse
│ ├── smartrecruiters.py # plateforme SmartRecruiters
│ └── <employeur>.py # 1 fichier = 1 employeur (sous-classe)
├── data/sources.json # registre descriptif des employeurs
├── frontend/ # React 18 + Vite + TS (carte MapLibre, sans jeton)
├── tests/ # pytest : normalisation + rejeu des connecteurs
├── scripts/check_headers.py # hook pre-commit : en-tête d'auteur obligatoire
└── docs/ # audit Lou·Ka, déploiementDémarrage rapide
git clone <repo> job-ka && cd job-ka
# Backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# Frontend
cd frontend && npm install && npm run build && cd ..
# Secrets (Scrapfly pour les sites anti-bot — facultatif au départ)
cp .env.example .env # puis remplir SCRAPFLY_API_KEY
# Amorçage : première synchronisation + géocodage
.venv/bin/python run.py sync
.venv/bin/python run.py geocode
# Service (API + frontend) — http://localhost:8096
.venv/bin/python run.py serve 8096En dev frontend : npm run dev dans frontend/ (proxy /api → :8096).
CLI run.py
| Commande | Rôle |
|---|---|
run.py sync [source ...] |
synchronise les offres (toutes les sources ou celles données) |
run.py watch [minutes] |
boucle : sync + géocodage + dédup (défaut 60 min) |
run.py serve [port] |
API + frontend (défaut 8096) |
run.py geocode [n] |
géocode les offres sans coordonnées |
run.py record <source ...> |
enregistre les fixtures de test d'une source |
Variables d'environnement
| Variable | Rôle |
|---|---|
SCRAPFLY_API_KEY |
rendu JS / anti-bot pour les pages carrières maison |
JOBKA_WORKDAY_DETAIL_LIMIT |
plafond de pages détail Workday par sync (défaut 300) |
JOBKA_SR_DETAIL_LIMIT |
plafond de pages détail SmartRecruiters (défaut 300) |
API
| Route | Rôle |
|---|---|
GET /health |
healthcheck (BD + fraîcheur des syncs) |
GET /api/jobs |
recherche : q, city, category, work_mode, employment_type, salary_min, with_salary, sort, limit, offset |
GET /api/jobs.geojson |
offres géolocalisées (carte), bbox + mêmes filtres |
GET /api/jobs/{uid} |
fiche complète + historique salarial |
GET /api/facets |
valeurs distinctes pour les filtres |
GET /api/sources |
registre des employeurs + fraîcheur par source |
GET /api/stats |
totaux, catégories, villes, dernières syncs |
POST /api/sync |
déclenche une synchronisation (webhook) |
Ajouter un employeur (≈ 10 lignes)
# jobka/connectors/mon_employeur.py (+ en-tête d'auteur, règle nº 1)
from .workday import WorkdayConnector
class MonEmployeurConnector(WorkdayConnector):
source_id = "mon_employeur"
EMPLOYER = "Mon Employeur inc."
TENANT = "monemployeur" # monemployeur.wd3.myworkdayjobs.com
HOST = "wd3"
SITE = "Careers"Puis : ajouter l'entrée dans data/sources.json, run.py sync mon_employeur
pour valider, run.py record mon_employeur pour figer les fixtures de test.
Tests
.venv/bin/python -m pytest # tout
.venv/bin/python -m pytest tests/test_normalize.py # normalisation seuletest_normalize.py— salaires, dates, modes, types, lieux, catégories ;test_connectors.py— rejeu hors ligne des fixtures : instantané exact + invariants (uid uniques, URLs absolues, salaires plausibles, valeurs jamais inventées) +finalize()sûr.
Production
job-ka-web .venv/bin/python run.py serve 8096 # API + frontend
job-ka-sync .venv/bin/python run.py watch 60 # resync horaire, jour et nuit
job-ka-ngrok ngrok http --url=www.job-ka.com 8096 # tunnel publicPhilosophie d'exploitation : on ne pousse que le code — le serveur maintient
ses données lui-même. data/*.db, frontend/dist/ et .env ne sont jamais
versionnés. Détails : docs/deploiement.md.
Principes
- Politesse — throttling par source, robots.txt respecté, User-Agent identifiable, budgets de requêtes, caches.
- Fidélité — les champs reflètent la source ; une donnée absente reste absente, jamais de valeur inventée (salaire, date, coordonnées).
- Traçabilité — chaque offre garde son URL originale ; la candidature se fait chez l'employeur. Zéro boîte noire.
- Robustesse — une source cassée ne bloque pas les autres et ne vide pas la base (délai de grâce + anti-dérive).
- Transparence salariale — salaires numériques normalisés en $/h et $/an ; filtre « salaire affiché » de premier ordre.
Écosystème Groupe KA
Job·Ka partage la philosophie et l'architecture des plateformes sœurs : Lou·Ka (logements), Immo·Ka, Vrai-Prix, ValoPlex, Auto·Ka, Fabri·Ka, Food·Ka.
Auteur
Simon-Pierre Boucher — contact@spboucher.ai
© 2026 Groupe KA — Simon-Pierre Boucher. Tous droits réservés.