- TypeScript 57%
- Svelte 42%
- PowerShell 0.4%
- CSS 0.2%
- Liquid 0.2%
- Other 0.1%
| .claude | ||
| .dev | ||
| .forgejo/workflows | ||
| .vscode | ||
| apps | ||
| .dockerignore | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| biome.json | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
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)
- Sur ton PostgreSQL local, cree le user et la base :
CREATE USER exkey WITH PASSWORD 'exkey'; CREATE DATABASE exkey OWNER exkey; - Installe les dependances depuis la racine :
pnpm install - Copie les fichiers d'environnement :
Renseigne au minimumcp .env.example .env cp apps/api/.env.example apps/api/.env cp apps/admin/.env.example apps/admin/.envBREVO_API_KEY/MAILER_ADDRESS_FROMdans.env(ouapps/api/.env). - Lance API + Admin en parallele :
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 depnpm devpnpm db:migrateau premier lancement.
URLs disponibles :
- Admin : http://localhost:5173
- API : http://localhost:3000
- Swagger : http://localhost:3000/docs
- Liveness probe : http://localhost:3000/health
Au premier acces a l'admin, tu es redirige automatiquement vers
/setuppour 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 hote5433par defaut pour ne pas entrer en conflit avec un Postgres local)exkey-api- NestJS sur3000exkey-admin- SvelteKit (adapter-node) sur5173
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 :
- API injoignable → page
/unavailable(avec message d'erreur et bouton "reessayer"). - API OK + non initialisee → wizard
/setup. - 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 dansexkey_metric_samples; un job quotidien (metrics.prune) purge les points au-delà deMETRICS_RETENTION_DAYS. - Endpoints owner-only :
GET /queue/stats(compteurs live) etGET /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 surSIGTERM/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-transformerpour 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émapgbossdé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 danssrc/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 partielexkey_tenants_singleton_idxexkey_establishments- etablissements rattaches a un tenantexkey_users- comptes utilisateurs (mot de passe hashe en argon2id)exkey_establishment_memberships- pivot user <-> etablissement avec roleexkey_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 dansapps/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
mainecrase 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.comvars.FORGEJO_OWNER- ex :exkeysecrets.FORGEJO_USERNAMEsecrets.FORGEJO_TOKEN
Le runner Forgejo doit etre demarre avec le label
runner09(option--labels runner09:dockerou equivalent dansconfig.yml) et supporter l'execution de jobs en container (node:22-alpineest utilise pour le job de lint/build).
Hors-scope (pour l'instant)
- Pas d'authentification fonctionnelle (le compte owner est cree mais la page
/loginest 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)