No description
  • TypeScript 57%
  • Svelte 42%
  • PowerShell 0.4%
  • CSS 0.2%
  • Liquid 0.2%
  • Other 0.1%
Find a file
François Bardel ca9ffac7e0
All checks were successful
CI / Lint & build (push) Successful in 1m14s
CI / Build & push Docker images (push) Successful in 2m41s
docs(memory): record the Mollie central OAuth redirect relay decision
2026-07-22 17:25:19 +02:00
.claude docs(memory): record the Mollie central OAuth redirect relay decision 2026-07-22 17:25:19 +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 feat(api): carry instance origin in Mollie OAuth state for central relay 2026-07-22 17:25:13 +02:00
.dockerignore chore: init project structure 2026-05-20 11:59:41 +02:00
.env.example feat(api): carry instance origin in Mollie OAuth state for central relay 2026-07-22 17:25:13 +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
biome.json chore: exclude svg asset files from biome checks 2026-07-03 16:43:40 +02:00
CLAUDE.md chore(agents): consolidate agents/ knowledge base into .claude/ 2026-07-10 13:30:20 +02:00
docker-compose.yml feat(setup): add optional demo data injection to first-run wizard 2026-07-17 17:06:14 +02:00
Dockerfile 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
package.json chore: update dependencies 2026-07-15 12:26:02 +02:00
pnpm-lock.yaml feat(api): send all transactional email through Brevo REST API, dropping nodemailer and SMTP relay 2026-07-21 17:42:37 +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(api): add category-grouped app settings with booking hold TTL editable by owner/admin (default 20min) 2026-07-21 17:43:01 +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
├── .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 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.

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

docker compose up --build

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.

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 avec photos placeholder, et 20 clients - 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 (parallele)
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
  • 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/)
  • Validation : Zod 4

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)
  • enum exkey_user_role (owner, admin, user)
  • enum exkey_app_setting_categories (bookings, payments, emails)

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)