Guide développeur
Ce guide s'adresse aux développeurs qui travaillent sur le code source de GovernIA. Il couvre le setup local, l'architecture du monorepo, les patterns de code et le workflow de déploiement.
Setup local
Prérequis
| Outil | Version | Usage |
|---|---|---|
| Node.js | 20 LTS | API, App, Backoffice |
| PostgreSQL | 14+ | Base de données |
| Git | — | Contrôle de version |
| Docker (optionnel) | 24+ | Environnement conteneurisé |
1. Cloner le dépôt
git clone https://github.com/nashvilleboy2019-art/governia.git
cd governia2. Installer les dépendances
Chaque package est indépendant — npm install doit être exécuté dans chaque dossier :
cd api && npm install && cd ..
cd app && npm install && cd ..
cd backoffice && npm install && cd ..3. Configurer les variables d'environnement
api/.env (variables minimales)
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=governia_dev
JWT_SECRET=dev_secret_longue_chaine_ici
JWT_EXPIRES_IN=7d
APP_FRONTEND_URL=http://localhost:3001
BO_FRONTEND_URL=http://localhost:3002
NODE_ENV=development
PORT=4000Toutes les intégrations (Agents IA, Stockage S3, Stripe, Jira) se configurent depuis Backoffice → Paramètres ou depuis l'interface tenant. Aucune variable
.envsupplémentaire n'est nécessaire pour démarrer l'API.
app/.env
NEXT_PUBLIC_API_URL=http://localhost:4000backoffice/.env
NEXT_PUBLIC_API_URL=http://localhost:40004. Démarrer les services
# Terminal 1 — API avec hot-reload
cd api && npm run start:dev
# Terminal 2 — App client
cd app && npm run dev
# Terminal 3 — Backoffice
cd backoffice && npm run dev5. Vérifier l'installation
# Santé de l'API
curl http://localhost:4000/health
# L'API répond aussi sur :
# http://localhost:4000/admin/* (backoffice)
# http://localhost:4000/client/* (app)
# http://localhost:4000/auth/* (public)Architecture
Monorepo
governia/
├── api/ NestJS 11 — backend REST
│ └── src/
│ ├── auth/
│ ├── users/
│ ├── missions/
│ ├── audits/
│ └── ... 32 modules au total
├── app/ Next.js 16 — portail client
│ └── app/
│ ├── (auth)/ Routes publiques (login, invite)
│ └── (client)/
│ ├── _utils/ authFetch, apiFetchJson
│ └── suite/ Workspace principal
│ ├── audit-ia/
│ ├── audit-reglementaire/
│ ├── audits-techniques/
│ ├── audit-rgpd/
│ ├── analytics/
│ ├── missions/
│ └── ...
├── backoffice/ Next.js 16 — interface admin
│ ├── app/
│ │ ├── (auth)/
│ │ └── (app)/ Pages admin protégées
│ ├── context/
│ │ └── AuthContext.tsx
│ └── lib/
│ └── api.ts
└── docs/ VitePressRoutes API
| Préfixe | Guard | Usage |
|---|---|---|
/auth/* | Aucun (public) | Connexion, inscription, acceptation d'invitation |
/client/* | JwtAuthGuard | Endpoints portail client |
/admin/* | JwtAuthGuard + AdminGuard | Endpoints backoffice |
Multi-tenancy
Principe
- Isolation par colonne
tenant_iddans toutes les tables métier - Chaque requête client transporte le header
X-Tenant-Id - Le header prend la priorité sur le
tenantIddu JWT
Pattern d'extraction du contexte
Dans les controllers client :
private ctx(user: any, req: Request): ClientCtx {
const hdrTenantId = this.parseHeaderTenantId(req); // lit 'x-tenant-id'
const tRaw = Number.isFinite(hdrTenantId) && hdrTenantId > 0
? hdrTenantId
: (user?.tenantId ?? user?.tenant_id ?? null);
return {
userId: Number(user?.id ?? user?.sub),
tenantId: tRaw == null ? null : Number(tRaw),
role: user?.role ?? null,
};
}Les controllers admin n'ont pas de restriction tenant — ils opèrent sur toutes les données.
Auth et guards
JWT payload
{
sub: number, // userId
email: string,
role: string, // 'ADMIN' | 'MANAGER_BU' | 'AUDITOR' | 'RESP_CLIENT' | 'OPERATOR'
tenantId?: number | null,
tenantType?: string | null,
}Décorer un controller
// Route authentifiée
@UseGuards(JwtAuthGuard)
@Get('something')
async getSomething(@CurrentUser() user: any) { ... }
// Admin uniquement (les deux guards sont requis — AdminGuard n'est pas implicite)
@UseGuards(JwtAuthGuard, AdminGuard)
@Get('admin/something')
async adminSomething() { ... }
// Avec vérification billing
@UseGuards(JwtAuthGuard, LimitGuard)
@Post('client/generate')
async generate() { ... }LimitGuard
- Lit le plan Stripe du tenant
- Ajoute
X-Usage-Warning: approaching-limità >90% d'usage - Lève
ForbiddenExceptionà >110%
Ajouter un module API
Structure standard
api/src/<feature>/
├── <feature>.entity.ts
├── <feature>.service.ts
├── <feature>.controller.ts
├── <feature>.module.ts
└── dto/
├── create-<feature>.dto.ts
└── update-<feature>.dto.tsExemple minimal
entity
@Entity('features')
export class Feature {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
tenant_id: number;
}service
@Injectable()
export class FeatureService {
constructor(
@InjectRepository(Feature)
private repo: Repository<Feature>,
) {}
findAll(tenantId: number) {
return this.repo.find({ where: { tenant_id: tenantId } });
}
}controller
@UseGuards(JwtAuthGuard)
@Controller('client/features')
export class FeatureController {
constructor(private svc: FeatureService) {}
@Get()
list(@CurrentUser() user: any, @Req() req: Request) {
const { tenantId } = this.ctx(user, req);
return this.svc.findAll(tenantId);
}
}module
@Module({
imports: [TypeOrmModule.forFeature([Feature])],
controllers: [FeatureController],
providers: [FeatureService],
})
export class FeatureModule {}Puis importer dans AppModule :
// api/src/app.module.ts
imports: [..., FeatureModule]Ajouter une page App
Pattern de page client
'use client';
export default function MyPage() {
const router = useRouter();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let alive = true;
const load = async () => {
try {
const result = await apiFetchJson<MyType>('/client/my-endpoint');
if (alive) setData(result);
} catch (e: any) {
if (e?.status === 401) router.replace('/login');
} finally {
if (alive) setLoading(false);
}
};
load();
return () => { alive = false; };
}, []);
if (loading) return <div>Chargement...</div>;
if (!data) return <div>Erreur</div>;
return <div>{/* contenu */}</div>;
}Helpers API client (app)
Toujours passer par app/app/(client)/_utils/authFetch.ts :
// Lecture de token avec fallbacks
getAuthToken()
// Fetch avec Authorization + X-Tenant-Id automatiques
authFetch('/client/endpoint', { method: 'POST', body: JSON.stringify(payload) })
// Fetch avec parsing JSON + gestion 204
apiFetchJson<T>('/client/endpoint')Ne jamais lire localStorage directement pour le token ou le tenantId.
Helper API client (backoffice)
// backoffice/lib/api.ts
import { fetchApi } from '@/lib/api';
const data = await fetchApi('/admin/endpoint');Base de données
TypeORM
synchronize: trueen développement — les tables sont créées/modifiées automatiquement- En production : migrations explicites (les entities ne doivent pas casser les données existantes)
Requêtes SQL brutes
Pour les queries complexes multi-jointures (rapports, analytics) :
const rows = await this.dataSource.query(`
SELECT
a.id AS "auditId",
a.title AS "auditTitle",
COUNT(r.id) AS "recommendationCount"
FROM audits a
LEFT JOIN recommendations r ON r.audit_id = a.id
WHERE a.tenant_id = $1
GROUP BY a.id, a.title
`, [tenantId]);Utiliser $1, $2... comme placeholders. Retourner des colonnes avec AS "camelCaseName".
Transactions
await this.dataSource.transaction(async (em) => {
const auditRepo = em.getRepository(Audit);
const assignRepo = em.getRepository(Assignment);
const audit = auditRepo.create({ ... });
await auditRepo.save(audit);
const assign = assignRepo.create({ auditId: audit.id, ... });
await assignRepo.save(assign);
});Tests
cd api
npm run test # Jest — tests unitaires
npm run test:watch # Mode watch
npm run test:cov # Rapport de couverture HTML (coverage/)
npm run test:e2e # Tests end-to-end
npm run lint # ESLint --fixDéploiement
Branches
| Branche | Cible | Déclencheur |
|---|---|---|
dev | VPS dev | Push + git pull + restart sur le VPS dev |
main | VPS prod | PR dev→main + merge + git pull + restart sur le VPS prod |
Commandes VPS
git pull origin main
# Rebuild et restart
docker compose -f docker-compose.prod.yml up -d --build api
docker compose -f docker-compose.prod.yml up -d --build backoffice
# App client — vider cache Next.js
docker volume rm app_next_cache bo_next_cache
docker compose -f docker-compose.prod.yml up -dNommer un service dans Docker Compose
| Service | Remarque |
|---|---|
client | C'est le portail client (pas app) |
backoffice | Backoffice admin |
api | Backend NestJS |
db | PostgreSQL |
Conventions de code
API
- Tous les controllers protégés déclarent
@UseGuards(JwtAuthGuard)— AdminGuard n'est jamais implicite - Les endpoints admin sont préfixés
/admin/, les endpoints client/client/ - La méthode
ctx(user, req)extrait{ userId, tenantId, role }depuis le JWT + header - Raw SQL préféré à QueryBuilder pour les queries de reporting (lisibilité, performant)
App/Backoffice
- L'app client utilise
apiFetchJson/authFetch— jamais defetchdirect - Le backoffice utilise
fetchApidepuislib/api.ts— jamais defetchdirect - Chaque page vérifie le token en début de
useEffectet redirige sur/loginsi absent - Utiliser un flag
aliveouisMountedRefpour éviter les setState après démontage
Style (backoffice)
Glassmorphism sombre :
rounded-3xl border border-white/10 bg-slate-950/70 p-5
shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-xlBadges de type : bleu pour CLIENT, violet pour CABINET. Badges de statut : émeraude pour ACTIVE, ambre pour PROSPECT.