No description
  • TypeScript 58.3%
  • Svelte 40.7%
  • CSS 0.3%
  • PowerShell 0.3%
  • Liquid 0.1%
Find a file
François Bardel fc96f8c05b
All checks were successful
CI / Lint & build (push) Successful in 1m21s
CI / Build & push Docker images (push) Successful in 2m48s
fix(api): report the real package version instead of 0.0.0
`process.env.npm_package_version` is only set when the process is spawned
through a package-manager script, so it was undefined in the Docker image
(which runs `node dist/main.js` directly) and `GET /` advertised the `0.0.0`
fallback in production.

Read the version from the package manifest on disk instead and make it the
single source of truth for both `GET /` and the Swagger document, which
duplicated it as a hardcoded literal.
2026-09-08 13:34:52 +02:00
.claude docs(agents): document Mollie MCP token env requirement 2026-09-02 17:46:25 +02:00
.dev chore: add logo source design files in .dev (Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>) 2026-07-03 11:02:53 +02:00
.forgejo/workflows chore: consolidate Dockerfile configurations for API and Admin services into a single Dockerfile and update build targets in docker-compose and CI workflows 2026-05-29 21:35:02 +02:00
.vscode chore: update admin app configuration by replacing ESLint with Biome for linting and formatting, and add new dev dependencies 2026-05-22 15:07:30 +02:00
apps fix(api): report the real package version instead of 0.0.0 2026-09-08 13:34:52 +02:00
.dockerignore chore: init project structure 2026-05-20 11:59:41 +02:00
.env.example fix(api): share the session cookie across sibling subdomains 2026-09-08 10:30:22 +02:00
.gitattributes chore: update admin app configuration by replacing ESLint with Biome for linting and formatting, and add new dev dependencies 2026-05-22 15:07:30 +02:00
.gitignore chore(api): remove dead code and a stale gitignore entry 2026-07-15 11:34:45 +02:00
.mcp.json chore: normalize .mcp.json line endings for biome 2026-07-27 17:30:33 +02:00
biome.json chore(deps): update workspace dependencies and pin @better-auth/utils 2026-08-13 16:56:24 +02:00
CLAUDE.md build(admin): pre-bundle the dynamic @lucide/svelte catalog import 2026-08-10 16:00:33 +02:00
docker-compose.yml fix(docker): always re-pull moving image tags on deploy 2026-09-08 10:49:11 +02:00
Dockerfile feat(widgets): standalone embeddable widget runtime app 2026-07-30 15:56:54 +02:00
package.json build(docker): make compose pull-only and source images from registry 2026-09-07 16:35:43 +02:00
pnpm-lock.yaml build(api): declare express as a direct dependency 2026-09-02 17:46:18 +02:00
pnpm-workspace.yaml feat(api): add gift vouchers module (catalogue, ledger, PDF vouchers, emails, expiry reminder) replacing the offers flag 2026-07-13 12:23:10 +02:00
README.md feat(docker): make public URLs configurable for internet deployments 2026-09-07 17:30:15 +02:00

exKey

Monorepo contenant l'API et l'interface admin du projet exKey.

exkey/
├── apps/
│   ├── api/      # NestJS 11 (Swagger, Drizzle ORM, PostgreSQL, Brevo API)
│   ├── admin/    # SvelteKit 2 + Svelte 5 + Tailwind v4 + Paraglide i18n
│   └── widgets/  # widgets de reservation embarquables (Svelte 5, bundles IIFE servis par l'API)
├── .claude/      # configuration des agents IA (rules / skills / memory)
├── .forgejo/     # workflows CI Forgejo Actions
└── docker-compose.yml

Pour les conventions internes (style, archi, i18n, naming DB…), voir CLAUDE.md et le dossier .claude/.

Prerequis

  • Node 22 LTS (ou superieur)
  • pnpm 10 (corepack enable && corepack prepare pnpm@10.4.1 --activate)
  • PostgreSQL 18 local pour le mode dev (ou Docker uniquement)
  • Docker + Docker Compose v2 pour la stack complete
  • Un compte Brevo avec une cle API (dashboard Brevo > SMTP & API > Cles API) - necessaire des le premier demarrage car le wizard de setup envoie un email de verification via l'API Brevo

Quickstart - mode dev natif (Postgres local)

  1. Sur ton PostgreSQL local, cree le user et la base :

    CREATE USER exkey WITH PASSWORD 'exkey';
    CREATE DATABASE exkey OWNER exkey;
    
  2. Installe les dependances depuis la racine :

    pnpm install
    
  3. Copie les fichiers d'environnement :

    cp .env.example .env
    cp apps/api/.env.example apps/api/.env
    cp apps/admin/.env.example apps/admin/.env
    

    Renseigne au minimum BREVO_API_KEY / MAILER_ADDRESS_FROM dans .env (ou apps/api/.env).

  4. Lance API + Admin + widgets en parallele :

    pnpm dev
    

    Les migrations Drizzle s'appliquent automatiquement au demarrage de l'API (un advisory lock Postgres evite les courses si plusieurs instances bootent en meme temps). Pas besoin de pnpm db:migrate au premier lancement.

    Cote admin, le premier chargement compile le graphe de modules a la demande : le shell de l'app est pre-compile des le demarrage du dev server (ligne 🔥 module graph warmed up), et toute page qui met plus de 2 s a repondre logge sa progression dans le terminal (⏳ compiling /page… 4s (717 modules so far) puis ✅ /page served in 7.4s). Pas de log = chargement instantane.

URLs disponibles :

Au premier acces a l'admin, tu es redirige automatiquement vers /setup pour creer le tenant, le premier etablissement et le compte owner. Tant que le setup n'est pas valide, toutes les routes redirigent vers le wizard.

Quickstart - stack complete via Docker Compose

pnpm compose:up

Le script builde d'abord les images exkey-api:latest / exkey-admin:latest depuis le Dockerfile racine (pnpm compose:build), puis lance docker compose up. Le docker-compose.yml ne contient volontairement aucune section build: (voir la section deploiement ci-dessous), donc un docker compose up --build seul ne builderait rien.

Cela demarre 3 conteneurs :

  • exkey-db - PostgreSQL 18 (port hote 5433 par defaut pour ne pas entrer en conflit avec un Postgres local)
  • exkey-api - NestJS sur 3000
  • exkey-admin - SvelteKit (adapter-node) sur 5173

Pour personnaliser les ports / credentials / SMTP, copie .env.example en .env a la racine et adapte les valeurs.

Deployer avec les images du registre (sans build local)

La CI pousse les images exkey-api / exkey-admin sur le registre OCI a chaque push sur main (tag dev) et a chaque release (latest + tag de version).

Le docker-compose.yml est pull-only (aucune section build:) : compose tire les images du registre meme quand la commande contient --build. C'est ce qui permet de le coller tel quel comme source compose « raw » sur une plateforme de deploiement (type Dokploy) qui impose docker compose up -d --build.

  • Plateforme de deploiement (source compose « raw ») : coller le contenu de docker-compose.yml, puis definir dans l'environnement du projet API_IMAGE, ADMIN_IMAGE, IMAGE_TAG (voir .env.example) et les autres variables requises (BETTER_AUTH_SECRET, POSTGRES_PASSWORD, etc.). La plateforme doit etre authentifiee sur le registre pour pouvoir pull.

    Pour exposer la stack sur internet, creer un domaine par service dans la plateforme (ex. admin.mondomaine.tld → service admin, api.mondomaine.tld → service api), en pointant chaque domaine sur le port conteneur 3000 (les deux apps ecoutent sur 3000 dans leur conteneur). Les URLs publiques n'ont pas de port : le reverse proxy de la plateforme termine le HTTPS sur 443. Definir ensuite dans l'environnement :

    ADMIN_URL=https://admin.mondomaine.tld
    API_PUBLIC_URL=https://api.mondomaine.tld
    TRUST_PROXY=true
    

    Le compose en derive ORIGIN/PUBLIC_API_URL (admin) et les defaults Better Auth (api). Les ports hotes API_HOST_PORT/ADMIN_HOST_PORT ne servent qu'a l'acces direct ip:port (debug) et sont independants des domaines - sur un serveur ou le port 3000 est deja pris (UI de la plateforme...), les changer ou ignorer l'acces direct.

  • Serveur avec clone du depot :

docker login <registre>
docker compose pull api admin
docker compose up -d

latest et dev etant des tags mouvants, relance docker compose pull avant chaque up pour recuperer la derniere image.

Premier demarrage : wizard de setup

L'app n'expose aucune table metier seedee. Au premier acces, le layout admin probe l'API (GET /setup/status) et oriente l'utilisateur selon une logique tri-etat :

  1. API injoignable → page /unavailable (avec message d'erreur et bouton "reessayer").
  2. API OK + non initialisee → wizard /setup.
  3. API OK + initialisee → page /login (l'authentification reelle arrivera dans une iteration suivante).

Le wizard execute trois pre-checks avant de soumettre :

  • GET /health - l'API repond ;
  • GET /setup/check/database - SELECT 1 + latence ;
  • POST /setup/check/smtp - envoi reel d'un email de test via le relai configure.

A la soumission, POST /setup cree dans une transaction exkey_tenants, exkey_establishments et l'enregistrement owner dans exkey_users (mot de passe hash en argon2id). Le tenant est singleton au niveau base via un index unique partiel - toute reinitialisation se fait en truncant les tables setup (voir apps/api/scripts/reset-setup.mjs).

Donnees de demonstration (optionnel)

Si l'API demarre avec SETUP_ALLOW_DEMO_DATA=true, le wizard affiche un switch « Injecter des donnees de demonstration » (desactive par defaut). Une fois coche, le setup injecte apres sa reussite un jeu de donnees d'exemple : 4 etablissements panaches (modules varies), leurs chambres / restaurants / spa / navettes / materiels de location avec photos placeholder, 20 clients et 28 avis clients (repondus pour partie, 2 bloques pour illustrer la moderation) - sans reservations ni factures. Le seed vit dans apps/api/src/setup/demo-data/ (UUIDs deterministes dd0000…, identifiables via WHERE id::text LIKE 'dd0000%').

L'injection n'est possible que pendant le setup : une fois l'instance initialisee, POST /setup repond 409 et aucun autre point d'entree (CLI ou endpoint) n'existe. Flag off ou switch decoche → l'application reste vierge de toute donnee de demo.

Jobs en arriere-plan & metriques

L'API embarque pg-boss pour exécuter des tâches hors du cycle HTTP. pg-boss tourne sur la même base PostgreSQL (via DATABASE_URL) mais dans un schéma dédié pgboss auto-créé/migré au démarrage - il n'interfère pas avec les migrations Drizzle et postgres.js reste le driver applicatif.

  • File email.send : l'envoi d'email (ex. invitations) est désormais enfilé puis drainé par un worker, avec retry automatique (5 essais, backoff). La requête HTTP ne bloque plus sur le relai SMTP.
  • Métriques : un job planifié (metrics.snapshot, toutes les 15 min par défaut) enregistre les compteurs de file dans exkey_metric_samples ; un job quotidien (metrics.prune) purge les points au-delà de METRICS_RETENTION_DAYS.
  • Endpoints owner-only : GET /queue/stats (compteurs live) et GET /metrics/series?metric=...&bucket=hour (historique agrégé).
  • Dashboard : la page d'accueil admin affiche, pour l'owner, des cartes KPI et un graphique (shadcn-svelte / LayerChart) du backlog email.
  • Arrêt gracieux : app.enableShutdownHooks() laisse pg-boss terminer les jobs en cours sur SIGTERM/SIGINT.

Scripts racine

Commande Description
pnpm dev Lance API + Admin en watch + bundles widgets (watch) + playground
pnpm widgets Watch des bundles widgets seul (sans le reste de la stack)
pnpm build Build toutes les apps
pnpm lint Lint Biome (verification)
pnpm lint:fix Lint Biome avec auto-fix
pnpm format Formattage Biome
pnpm test Tests toutes les apps
pnpm db:generate Genere une migration Drizzle depuis src/db/schema.ts
pnpm db:migrate Applique les migrations en attente (rarement utile : auto au boot)
pnpm compose:up Build + demarre la stack Docker
pnpm compose:down Arrete la stack Docker

Stack technique

API (apps/api)

  • NestJS 11, TypeScript, ESM via nest build
  • Swagger / OpenAPI 3 sur /docs
  • Drizzle ORM 0.45 + driver postgres (postgres.js)
  • Migrations Drizzle Kit, appliquees automatiquement au demarrage (advisory lock)
  • Validation Zod 4 (env + DTO)
  • class-validator / class-transformer pour les DTO Nest
  • Hash de mot de passe via @node-rs/argon2 (argon2id)
  • Emails transactionnels via l'API REST Brevo (BREVO_API_KEY)
  • Jobs en arrière-plan + planification via pg-boss (file sur PostgreSQL, schéma pgboss dédié)
  • Métriques persistées (séries temporelles) dans exkey_metric_samples
  • Recherche globale : GET /api/search (session uniquement) - requêtes ILIKE multi-établissements, résultats groupés par type d'entité (registre extensible dans src/search/searchers.ts)
  • Healthchecks via @nestjs/terminus

Admin (apps/admin)

  • SvelteKit 2 + Svelte 5 (runes), adapter-node, Vite
  • Tailwind CSS v4 (plugin Vite officiel) + @tailwindcss/forms + @tailwindcss/typography
  • Composants UI : shadcn-svelte + bits-ui + formsnap + sveltekit-superforms
  • Toasts : svelte-sonner
  • Theme clair/sombre : mode-watcher
  • Icones : @lucide/svelte
  • i18n : Paraglide JS (locales en / fr / de / es / it / pt, detection cookie + Accept-Language, runtime compile dans src/lib/paraglide/)
  • Recherche globale Ctrl+K / Cmd+K : palette centrée (loupe dans l'en-tête) avec recherche live sur les réservations, clients, bons cadeaux, produits, chambres, restaurants, prestations spa, navettes, matériels de location, widgets, établissements, pages et documentation (docs proposées seulement si le réglage interface.show_documentation est actif) ; bascule automatique d'établissement au clic (registre extensible dans src/lib/search/registry.ts)
  • Tableau de bord en deux onglets : Aujourd'hui (arrivées/départs, clients présents, occupation ce soir, paiements en attente, volumes du jour des modules activés) et Performance, le comparatif période vs période - préréglages (7 j, 30 j, mois en cours, mois dernier, année) ou plage libre, bascule « vs période précédente » / « vs même période N-1 », portée établissement courant ou tous les établissements (totaux + ventilation par établissement, montants totalisés seulement à devise commune), graphique en superposition CA/réservations. Les agrégats sont calculés à la volée côté API (GET /establishments/:id/bookings/stats/compare et GET /bookings/stats/compare) : CA des réservations confirmées/terminées à la date de confirmation, bons cadeaux comptés à la date d'émission (séparés du CA réservations), état de la vue conservé dans l'URL
  • Module Locations (location de matériel : skis, vélos, bateaux...) : chaque fiche décrit un matériel générique dont les déclinaisons sont des variantes portant le stock, avec une unité de location fixée à la création (heure, demi-journée, journée ou nuit). Le tarif de base par unité peut être complété par des tarifs saisonniers réutilisant les saisons de l'établissement (gérées dans l'onglet « Saisons » de sa page de paramètres) ; le tout est intégré au tunnel de réservation admin (étape « Locations » du wizard, prix figé à l'ajout)
  • Avis clients (Webmarketing > Avis clients) : notes 1-5 étoiles + commentaire, ciblant l'établissement ou un service (chambres, restaurant, spa, navettes, locations). Note globale et moyennes par service au-dessus du tableau (filtres sujet/note/réponse/modération), réponse unique et définitive de l'établissement (rôle gestionnaire, verrouillée côté API), modération réversible (avis bloqué = exclu de toutes les moyennes et du futur affichage public). La fiche client (onglets Profil / Préférences / Statistiques / Réservations / Avis) reprend les avis du client, et le tableau de bord affiche la note moyenne de chaque établissement. Le dépôt d'avis public (widget post-séjour) viendra plus tard
  • Validation : Zod 4

Widgets embarquables (apps/widgets)

  • Trois widgets de réservation rapide à intégrer sur des sites web tiers : Hôtel - Quick Search (dispo chambres par plage de dates + personnes, meilleur prix affiché à côté du bouton de recherche), Navette (date + passagers) et Restaurant (restaurant + couverts + date + heure), chacun conditionné au module correspondant de l'établissement ; formulaire de recherche sur une seule ligne dès que la place le permet
  • Une soumission crée une vraie réservation en statut pending_approval (canal widget) qui bloque l'inventaire ; le staff l'accepte ou la refuse depuis l'admin (notification + emails invité localisés en 6 langues) ; expiration automatique via le réglage bookings.approval_ttl_hours
  • Bundles IIFE autonomes (Svelte 5 + Tailwind v4 + shadcn-svelte, CSS inliné) buildés dans apps/widgets/dist/v1/ et servis par l'API sous /public/widgets/v1/<type>.js (URL stable, ACAO *) ; pnpm dev les builde et les garde en watch automatiquement (one-shot : pnpm --filter @exkey/widgets build)
  • Intégration en collant deux lignes (div + script) ; deux modes par widget : Shadow DOM (recommandé) ou iframe (page frame hébergée par l'API, hauteur auto via postMessage)
  • Personnalisation par widget depuis l'admin (couleurs, arrondi, ombre, bordure, sélecteur de dates) appliquée sans re-copier le code ; overrides par page via data-attributes (data-lang, data-primary-color, data-embed-mode, …) exposés dans l'éditeur par l'interrupteur « Inclure les options par page », pré-remplis en direct avec les réglages du formulaire (attribut vide = réglage enregistré, valides en shadow comme en iframe)
  • Langues servies = section Langues de l'établissement (sous-ensemble des 6 locales + langue par défaut)
  • Surface publique anonyme /api/public/widgets/:publicKey/* (config, disponibilités avec restrictions de vente appliquées, soumission) - clé publique 192 bits, rate limiting en mémoire, CORS ouvert sans credentials ; une clé de prévisualisation admin (?preview=wgtp_..., exposée uniquement par l'API authentifiée) débloque les lectures d'un widget en pause pour l'aperçu de l'éditeur, jamais la soumission
  • Playground de dev : démarré par pnpm dev (port 5174, page hôte volontairement hostile) ; seul : pnpm --filter @exkey/widgets playground

Tooling

  • pnpm workspaces (Node 22 LTS)
  • Biome 2 (lint + format unifies pour tout le repo)
  • Docker multi-stage avec cache pnpm
  • CI : Forgejo Actions (lint + build + push registre OCI)

Variables d'environnement

.env racine

Consomme par docker-compose.yml et par l'API en mode dev natif (via dotenv en cascade). Voir .env.example pour la liste complete.

# PostgreSQL
POSTGRES_USER=exkey
POSTGRES_PASSWORD=exkey
POSTGRES_DB=exkey
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_SSL=false
POSTGRES_HOST_PORT=5433   # port hote du conteneur db
API_HOST_PORT=3000
ADMIN_HOST_PORT=5173

# Medias uploades (stockage disque local, servis sous /uploads)
UPLOAD_DIR=./var/uploads          # volume monte (/app/uploads) en Docker
UPLOAD_MAX_FILE_SIZE_MB=10

# Donnees de demo au setup : si true, le wizard /setup propose un switch
# "Injecter des donnees de demonstration". A laisser false en production.
SETUP_ALLOW_DEMO_DATA=false

# Emails transactionnels (API REST Brevo)
BREVO_API_KEY=
MAILER_ADDRESS_FROM="exKey <no-reply@example.com>"

apps/api/.env

Surcharges propres a l'API. La connexion Postgres accepte soit DATABASE_URL (prioritaire), soit les variables POSTGRES_* separees.

NODE_ENV=development
PORT=3000
# DATABASE_URL=postgres://exkey:exkey@localhost:5432/exkey
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=exkey
POSTGRES_PASSWORD=exkey
POSTGRES_DB=exkey
POSTGRES_SSL=false

# Jobs en arriere-plan (pg-boss) - valeurs par defaut, override optionnel
PGBOSS_SCHEMA=pgboss
PGBOSS_MAX=5

# Metriques (exkey_metric_samples)
METRICS_SNAPSHOT_CRON=*/15 * * * *
METRICS_RETENTION_DAYS=180

# Medias uploades (stockage disque local, servis sous /uploads)
UPLOAD_DIR=./var/uploads
UPLOAD_MAX_FILE_SIZE_MB=10

# Donnees de demo au setup (switch visible sur /setup uniquement si true)
SETUP_ALLOW_DEMO_DATA=false

# Emails transactionnels (override possible par rapport au .env racine)
BREVO_API_KEY=
MAILER_ADDRESS_FROM="exKey <no-reply@example.com>"

apps/admin/.env

PUBLIC_API_URL=http://localhost:3000

Schema base de donnees

Toutes les tables et enums crees par l'app sont prefixes exkey_ et utilisent le pluriel snake_case (cf. .claude/rules/db-naming.md).

Tables actuellement gerees (apps/api/src/db/schema/) :

  • exkey_tenants - singleton via index unique partiel exkey_tenants_singleton_idx
  • exkey_establishments - etablissements rattaches a un tenant
  • exkey_users - comptes utilisateurs (mot de passe hashe en argon2id)
  • exkey_establishment_memberships - pivot user <-> etablissement avec role
  • exkey_metric_samples - séries temporelles génériques (métriques du dashboard)
  • exkey_app_settings - paramètres applicatifs regroupés par catégorie (page admin Paramètres, owner/admin ; le réglage interface.show_documentation masque la documentation de la sidebar et de la recherche globale)
  • exkey_widgets - widgets de réservation embarquables (type, thème jsonb, clé publique, mode d'intégration)
  • exkey_reviews - avis clients (note 1-5, sujet, réponse unique verrouillée, modération réversible via hidden_at)
  • enum exkey_user_role (owner, admin, user)
  • enum exkey_app_setting_categories (bookings, payments, emails, interface)

Le schéma pgboss (tables de la file de jobs) est géré automatiquement par pg-boss et n'apparaît pas dans apps/api/src/db/schema/ ni dans les migrations Drizzle.

Pour reinitialiser le setup en local :

node apps/api/scripts/reset-setup.mjs

Cette commande tronque les 4 tables setup en cascade. A ne jamais utiliser hors developpement.

CI Forgejo

Le workflow .forgejo/workflows/ci.yml tourne sur un runner self-hosted labelise runner09 (runs-on: runner09).

Declencheurs et etapes

  • PR vers main : pnpm install + pnpm -r build (pas de push d'image).
  • Push sur main : lint/build, puis build + push des deux images Docker.
  • Release publiee (release: { types: [published] }) : lint/build, puis build + push des deux images Docker.

Strategie de tags Docker

Evenement Tags pousses sur exkey-api et exkey-admin
push sur main (commit simple) dev
release publiee (tag ex. v1.2.3) latest et v1.2.3 (le nom du tag de release)

Concretement :

  • Un commit sur main ecrase l'image :dev => image "rolling" de la branche principale.
  • Une release ecrase :latest (= derniere version stable) et publie egalement une image immuable taguee avec la version (:v1.2.3), pour pouvoir revenir en arriere ou epingler precisement une version en prod.

Secrets/variables a definir dans Forgejo

  • vars.FORGEJO_REGISTRY - ex : forgejo.example.com
  • vars.FORGEJO_OWNER - ex : exkey
  • secrets.FORGEJO_USERNAME
  • secrets.FORGEJO_TOKEN

Le runner Forgejo doit etre demarre avec le label runner09 (option --labels runner09:docker ou equivalent dans config.yml) et supporter l'execution de jobs en container (node:22-alpine est utilise pour le job de lint/build).

Hors-scope (pour l'instant)

  • Pas d'authentification fonctionnelle (le compte owner est cree mais la page /login est un squelette) - CORS permissif (origin: true, credentials: true)
  • Pas de tests automatises ecrits
  • Pas de packages/shared (a extraire plus tard si besoin)
  • Pas de table metier au-dela des entites du setup et de la table de metriques (exkey_metric_samples)