diff --git a/src/features/admin/pages.js b/src/features/admin/AdminPage.client.js
similarity index 100%
rename from src/features/admin/pages.js
rename to src/features/admin/AdminPage.client.js
diff --git a/src/features/admin/page.js b/src/features/admin/AdminPage.server.js
similarity index 100%
rename from src/features/admin/page.js
rename to src/features/admin/AdminPage.server.js
diff --git a/src/features/admin/actions.js b/src/features/admin/actions.js
deleted file mode 100644
index da1e4b0..0000000
--- a/src/features/admin/actions.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Admin Server Actions
- *
- * Exported separately from admin/index.js to avoid bundling
- * server-side code (which includes database imports) into client components.
- *
- * Usage: import { getDashboardStats } from '@zen/core/features/admin/actions';
- */
-
-export { getDashboardStats } from './actions/statsActions.js';
diff --git a/src/features/admin/actions/statsActions.js b/src/features/admin/actions/statsActions.js
deleted file mode 100644
index f6832a2..0000000
--- a/src/features/admin/actions/statsActions.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Admin Stats Actions
- *
- * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
- * Ce fichier est conservé pour la rétrocompatibilité avec les projets qui
- * appellent getDashboardStats() directement depuis leur page admin.
- */
-
-'use server';
-
-import { query } from '@zen/core/database';
-import { fail } from '@zen/core/shared/logger';
-
-/**
- * @deprecated Utiliser collectAllDashboardData() de serverRegistry à la place.
- * @returns {Promise<{ success: boolean, stats?: { totalUsers: number }, error?: string }>}
- */
-export async function getDashboardStats() {
- try {
- const result = await query(`SELECT COUNT(*) as count FROM zen_auth_users`);
- const totalUsers = parseInt(result.rows[0].count) || 0;
- return { success: true, stats: { totalUsers } };
- } catch (error) {
- fail(`Error getting dashboard stats: ${error.message}`);
- return { success: false, error: error.message || 'Failed to get dashboard statistics' };
- }
-}
diff --git a/src/features/admin/components/AdminPages.js b/src/features/admin/components/AdminPages.js
deleted file mode 100644
index 6772d25..0000000
--- a/src/features/admin/components/AdminPages.js
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-
-import DashboardPage from './pages/DashboardPage.js';
-import UsersPage from './pages/UsersPage.js';
-import UserEditPage from './pages/UserEditPage.js';
-import ProfilePage from './pages/ProfilePage.js';
-import RolesPage from './pages/RolesPage.js';
-import RoleEditPage from './pages/RoleEditPage.js';
-
-export default function AdminPagesClient({ params, user, dashboardStats = null }) {
- const parts = params?.admin || [];
- const page = parts[0] || 'dashboard';
-
- if (page === 'users' && parts[1] === 'edit' && parts[2]) {
- return ;
- }
-
- if (page === 'roles' && parts[1] === 'edit' && parts[2]) {
- return ;
- }
-
- if (page === 'roles' && parts[1] === 'new') {
- return ;
- }
-
- const corePages = {
- dashboard: () => ,
- users: () => ,
- profile: () => ,
- roles: () => ,
- };
-
- const CorePageComponent = corePages[page];
- return CorePageComponent ? : ;
-}
diff --git a/src/features/admin/components/AdminPagesLayout.js b/src/features/admin/components/AdminShell.js
similarity index 100%
rename from src/features/admin/components/AdminPagesLayout.js
rename to src/features/admin/components/AdminShell.js
diff --git a/src/features/admin/dashboard/clientRegistry.js b/src/features/admin/dashboard/clientRegistry.js
deleted file mode 100644
index 4b0f806..0000000
--- a/src/features/admin/dashboard/clientRegistry.js
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-/**
- * Dashboard Client Registry — API générique
- *
- * Les features s'enregistrent ici via registerClientWidget().
- * Ce fichier ne doit jamais importer une feature directement.
- * Les features dépendent de ce fichier, pas l'inverse.
- */
-
-const widgets = [];
-
-/**
- * Enregistre un composant React pour le tableau de bord.
- * Appelé en side effect par chaque feature (ex: auth/dashboard.widget.js).
- * @param {string} id - Identifiant unique de la feature (doit correspondre à l'id serveur)
- * @param {React.ComponentType} Component - Composant avec props { data, loading }
- * @param {number} order - Ordre d'affichage (croissant). Utiliser des intervalles de 10.
- */
-export function registerClientWidget(id, Component, order) {
- widgets.push({ id, Component, order });
-}
-
-/**
- * Retourne tous les widgets enregistrés, triés par order croissant.
- * @returns {Array<{ id: string, Component: React.ComponentType, order: number }>}
- */
-export function getClientWidgets() {
- return [...widgets].sort((a, b) => a.order - b.order);
-}
diff --git a/src/features/admin/dashboard/registry.js b/src/features/admin/dashboard/registry.js
deleted file mode 100644
index af41447..0000000
--- a/src/features/admin/dashboard/registry.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Dashboard Server Registry — API générique
- *
- * Les features s'enregistrent ici via registerServerWidget().
- * Ce fichier ne doit jamais importer une feature directement.
- * Les features dépendent de ce fichier, pas l'inverse.
- */
-
-const widgets = new Map(); // id → fetcher
-
-/**
- * Enregistre une fonction de fetch pour le tableau de bord.
- * Appelé en side effect par chaque feature (ex: auth/dashboard.server.js).
- * @param {string} id - Identifiant unique de la feature
- * @param {() => Promise} fetcher - Fonction async retournant des données sérialisables
- */
-export function registerServerWidget(id, fetcher) {
- widgets.set(id, fetcher);
-}
-
-/**
- * Exécute tous les fetchers enregistrés en parallèle.
- * Un fetcher qui échoue n'empêche pas les autres (Promise.allSettled).
- * @returns {Promise>} Map featureId → données
- */
-export async function collectAllDashboardData() {
- const results = await Promise.allSettled(
- Array.from(widgets.entries()).map(([id, fetcher]) =>
- fetcher().then(data => ({ id, data }))
- )
- );
-
- return results.reduce((acc, result) => {
- if (result.status === 'fulfilled') {
- acc[result.value.id] = result.value.data;
- }
- return acc;
- }, {});
-}
diff --git a/src/features/admin/dashboard/serverRegistry.js b/src/features/admin/dashboard/serverRegistry.js
deleted file mode 100644
index 427244c..0000000
--- a/src/features/admin/dashboard/serverRegistry.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Dashboard Server Registry — Point d'entrée
- *
- * Ré-exporte l'API du registre pur et déclenche le câblage des features
- * via un import side-effect de features/dashboard.server.js.
- *
- * Ne jamais importer une feature directement ici.
- * Pour ajouter une feature : éditer src/features/dashboard.server.js.
- */
-
-export { collectAllDashboardData } from './registry.js';
-
-// Side effect : initialise le registre, enregistre les widgets core, puis les features
-import './registry.js';
-import './widgets/index.server.js';
-import '../../dashboard.server.js';
diff --git a/src/features/admin/dashboard/widgets/index.client.js b/src/features/admin/dashboard/widgets/index.client.js
deleted file mode 100644
index 3077164..0000000
--- a/src/features/admin/dashboard/widgets/index.client.js
+++ /dev/null
@@ -1,10 +0,0 @@
-'use client';
-
-/**
- * Widgets core — Câblage client
- *
- * Ce fichier est le SEUL à modifier pour ajouter un widget core côté client.
- * Les widgets core sont auto-enregistrés au démarrage de l'admin, sans module externe.
- */
-
-import './users.widget.js';
diff --git a/src/features/admin/dashboard/widgets/index.server.js b/src/features/admin/dashboard/widgets/index.server.js
deleted file mode 100644
index f84abc1..0000000
--- a/src/features/admin/dashboard/widgets/index.server.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Widgets core — Câblage serveur
- *
- * Ce fichier est le SEUL à modifier pour ajouter un widget core côté serveur.
- * Les widgets core sont auto-enregistrés au démarrage de l'admin, sans module externe.
- */
-
-import './users.server.js';
diff --git a/src/features/admin/navigation.server.js b/src/features/admin/navigation.server.js
deleted file mode 100644
index dc6c0c4..0000000
--- a/src/features/admin/navigation.server.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Admin Navigation Builder (Server-Only)
- *
- * IMPORTANT: Navigation data must be serializable (no functions/components).
- * Icons are passed as string names and resolved on the client.
- */
-
-/**
- * Build complete navigation sections
- * @param {string} pathname - Current pathname
- * @returns {Array} Navigation sections (serializable, icons as strings)
- */
-export function buildNavigationSections(pathname) {
- const coreNavigation = [
- {
- id: 'Dashboard',
- title: 'Tableau de bord',
- icon: 'DashboardSquare03Icon',
- items: [
- {
- name: 'Tableau de bord',
- href: '/admin/dashboard',
- icon: 'DashboardSquare03Icon',
- current: pathname === '/admin/dashboard'
- },
- ]
- }
- ];
-
- const systemNavigation = [
- {
- id: 'users',
- title: 'Utilisateurs',
- icon: 'UserMultiple02Icon',
- items: [
- {
- name: 'Utilisateurs',
- href: '/admin/users',
- icon: 'UserMultiple02Icon',
- current: pathname.startsWith('/admin/users')
- },
- {
- name: 'Rôles',
- href: '/admin/roles',
- icon: 'Crown03Icon',
- current: pathname.startsWith('/admin/roles')
- },
- ]
- }
- ];
-
- return [...coreNavigation, ...systemNavigation];
-}
diff --git a/src/features/admin/components/pages/DashboardPage.js b/src/features/admin/pages/DashboardPage.client.js
similarity index 100%
rename from src/features/admin/components/pages/DashboardPage.js
rename to src/features/admin/pages/DashboardPage.client.js
diff --git a/src/features/admin/components/pages/ProfilePage.js b/src/features/admin/pages/ProfilePage.client.js
similarity index 100%
rename from src/features/admin/components/pages/ProfilePage.js
rename to src/features/admin/pages/ProfilePage.client.js
diff --git a/src/features/admin/components/pages/RoleEditPage.js b/src/features/admin/pages/RoleEditPage.client.js
similarity index 100%
rename from src/features/admin/components/pages/RoleEditPage.js
rename to src/features/admin/pages/RoleEditPage.client.js
diff --git a/src/features/admin/components/pages/RolesPage.js b/src/features/admin/pages/RolesPage.client.js
similarity index 100%
rename from src/features/admin/components/pages/RolesPage.js
rename to src/features/admin/pages/RolesPage.client.js
diff --git a/src/features/admin/components/pages/UserEditPage.js b/src/features/admin/pages/UserEditPage.client.js
similarity index 100%
rename from src/features/admin/components/pages/UserEditPage.js
rename to src/features/admin/pages/UserEditPage.client.js
diff --git a/src/features/admin/components/pages/UsersPage.js b/src/features/admin/pages/UsersPage.client.js
similarity index 100%
rename from src/features/admin/components/pages/UsersPage.js
rename to src/features/admin/pages/UsersPage.client.js
diff --git a/src/features/admin/middleware/protect.js b/src/features/admin/protect.js
similarity index 100%
rename from src/features/admin/middleware/protect.js
rename to src/features/admin/protect.js
diff --git a/src/features/admin/dashboard/widgets/users.widget.js b/src/features/admin/widgets/users.client.js
similarity index 100%
rename from src/features/admin/dashboard/widgets/users.widget.js
rename to src/features/admin/widgets/users.client.js
diff --git a/src/features/admin/dashboard/widgets/users.server.js b/src/features/admin/widgets/users.server.js
similarity index 100%
rename from src/features/admin/dashboard/widgets/users.server.js
rename to src/features/admin/widgets/users.server.js
diff --git a/src/features/auth/pages.js b/src/features/auth/AuthPage.client.js
similarity index 100%
rename from src/features/auth/pages.js
rename to src/features/auth/AuthPage.client.js
diff --git a/src/features/auth/page.js b/src/features/auth/AuthPage.server.js
similarity index 100%
rename from src/features/auth/page.js
rename to src/features/auth/AuthPage.server.js
diff --git a/src/features/auth/actions/authActions.js b/src/features/auth/actions/authActions.js
deleted file mode 100644
index 0b22439..0000000
--- a/src/features/auth/actions/authActions.js
+++ /dev/null
@@ -1,434 +0,0 @@
-/**
- * Server Actions for Next.js
- * Authentication actions for login, register, password reset, etc.
- */
-
-'use server';
-
-import { register, login, requestPasswordReset, resetPassword, verifyUserEmail } from '../lib/auth.js';
-import { validateSession, deleteSession } from '../lib/session.js';
-import { verifyEmailToken, verifyResetToken, sendVerificationEmail, sendPasswordResetEmail } from '../lib/email.js';
-import { fail } from '@zen/core/shared/logger';
-import { cookies, headers } from 'next/headers';
-import { getSessionCookieName, getPublicBaseUrl } from '@zen/core/shared/config';
-import { checkRateLimit, getIpFromHeaders, formatRetryAfter } from '@zen/core/shared/rate-limit';
-
-/**
- * Errors that are safe to surface verbatim to the client (e.g. "token expired").
- * All other errors — including library-layer and database errors — must be caught,
- * logged server-side only, and replaced with a generic message to prevent internal
- * detail disclosure.
- */
-class UserFacingError extends Error {
- constructor(message) {
- super(message);
- this.name = 'UserFacingError';
- }
-}
-
-/**
- * Get the client IP from the current server action context.
- */
-async function getClientIp() {
- const h = await headers();
- return getIpFromHeaders(h);
-}
-
-// Emitted at most once per process lifetime to avoid log flooding while still
-// alerting operators that per-IP rate limiting is inactive for server actions.
-let _rateLimitUnavailableWarned = false;
-
-/**
- * Apply per-IP rate limiting only when a real IP address is available.
- *
- * When IP resolves to 'unknown' (no trusted proxy configured), every caller
- * shares the single bucket keyed ':unknown'. A single attacker can
- * exhaust that bucket in 5 requests and impose a 30-minute denial-of-service
- * on every legitimate user. Rate limiting is therefore suspended for the
- * 'unknown' case and a one-time operator warning is emitted instead,
- * mirroring the identical policy applied to API routes in router.js.
- *
- * Set ZEN_TRUST_PROXY=true only behind a verified reverse proxy (Nginx,
- * Cloudflare, AWS ALB, …) that strips and rewrites forwarding headers.
- *
- * @param {string} ip
- * @param {string} action
- * @returns {{ allowed: boolean, retryAfterMs?: number } | null} null = suspended
- */
-function enforceRateLimit(ip, action) {
- if (ip === 'unknown') {
- if (!_rateLimitUnavailableWarned) {
- _rateLimitUnavailableWarned = true;
- fail(
- 'Rate limiting inactive (server actions): client IP cannot be determined. ' +
- 'Set ZEN_TRUST_PROXY=true behind a verified reverse proxy to enable per-IP rate limiting.'
- );
- }
- return null;
- }
- return checkRateLimit(ip, action);
-}
-
-/**
- * Validate anti-bot fields submitted with forms.
- * - _hp : honeypot field — must be empty
- * - _t : form load timestamp (ms) — submission must be at least 1.5 s after page
- * load AND no more than MAX_FORM_AGE_MS in the past. Both a lower bound
- * (prevents instant automated submission) and an upper bound (prevents the
- * trivial bypass of supplying an arbitrary past timestamp such as _t=1) are
- * enforced. Future timestamps are also rejected.
- *
- * @param {FormData} formData
- * @returns {{ valid: boolean, error?: string }}
- */
-function validateAntiBotFields(formData) {
- const honeypot = formData.get('_hp');
- if (honeypot && honeypot.length > 0) {
- return { valid: false, error: 'Requête invalide' };
- }
-
- const MIN_ELAPSED_MS = 1_500;
- const MAX_FORM_AGE_MS = 10 * 60 * 1_000; // 10 minutes — rejects epoch-era timestamps
- const now = Date.now();
- const t = parseInt(formData.get('_t') || '0', 10);
- const elapsed = now - t;
-
- if (t === 0 || t > now || elapsed < MIN_ELAPSED_MS || elapsed > MAX_FORM_AGE_MS) {
- return { valid: false, error: 'Requête invalide' };
- }
-
- return { valid: true };
-}
-
-// Get cookie name from environment or use default
-export const COOKIE_NAME = getSessionCookieName();
-
-/**
- * Register a new user
- * @param {FormData} formData - Form data with email, password, name
- * @returns {Promise