iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenFiler

Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.

Next.js 15TypeScriptSQLiteBetter AuthTailwind CSS v4
GitHub
01Setup et configuration02Authentification avec Better Auth03Base de données SQLite04Système d'upload de fichiers05Dashboard et interface06Corbeille et liens de partage07Déploiement Docker
Chapitre 3·20 min

Base de données SQLite

OpenFiler utilise SQLite avec better-sqlite3 — synchrone, sans serveur, parfait pour une app mono-utilisateur. Pas de promesses, pas d'ORM, juste du SQL direct.

Pourquoi SQLite

Pour une app de gestion de fichiers personnelle, SQLite est le bon choix : zéro configuration, fichier unique, performances excellentes en lecture. Si le projet évolue vers du multi-utilisateur, migrer vers PostgreSQL se fait en changeant l'adaptateur Better Auth et le driver dans seed.ts.

Initialisation au démarrage

instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { seed } = await import('./lib/seed')
    seed()
  }
}

Next.js exécute instrumentation.ts une seule fois au démarrage du serveur. La condition NEXT_RUNTIME === 'nodejs' évite l'exécution dans l'edge runtime.

Création des tables

lib/seed.ts
import Database from 'better-sqlite3'
 
const db = new Database(process.env.DATABASE_URL!)
 
export function seed() {
  db.exec(`
    CREATE TABLE IF NOT EXISTS file_metadata (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      filename TEXT NOT NULL,
      original_name TEXT NOT NULL,
      folder TEXT NOT NULL,
      size INTEGER NOT NULL,
      mime_type TEXT NOT NULL,
      is_public INTEGER DEFAULT 0,
      uploaded_at TEXT DEFAULT (datetime('now')),
      uploaded_by TEXT
    );
 
    CREATE TABLE IF NOT EXISTS trash (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      filename TEXT NOT NULL,
      original_name TEXT NOT NULL,
      folder TEXT NOT NULL,
      size INTEGER NOT NULL,
      deleted_at TEXT DEFAULT (datetime('now')),
      deleted_by TEXT
    );
 
    CREATE TABLE IF NOT EXISTS share_link (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      token TEXT UNIQUE NOT NULL,
      filename TEXT NOT NULL,
      folder TEXT NOT NULL,
      expires_at TEXT NOT NULL,
      created_at TEXT DEFAULT (datetime('now'))
    );
 
    CREATE TABLE IF NOT EXISTS file_view (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      filename TEXT NOT NULL,
      folder TEXT NOT NULL,
      ip TEXT,
      user_agent TEXT,
      referer TEXT,
      viewed_at TEXT DEFAULT (datetime('now')),
      type TEXT DEFAULT 'view'
    );
 
    CREATE TABLE IF NOT EXISTS login_attempt (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      ip TEXT NOT NULL,
      email TEXT,
      success INTEGER DEFAULT 0,
      attempted_at TEXT DEFAULT (datetime('now'))
    );
 
    CREATE TABLE IF NOT EXISTS banned_ip (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      ip TEXT UNIQUE NOT NULL,
      reason TEXT,
      banned_at TEXT DEFAULT (datetime('now')),
      expires_at TEXT
    );
  `)
 
  // Créer l'admin par défaut si il n'existe pas
  const existing = db.prepare('SELECT id FROM user WHERE email = ?').get('admin@openfiler.local')
  if (!existing) {
    // Better Auth gère le hash — on passe par son API
    createDefaultAdmin()
  }
}

CREATE TABLE IF NOT EXISTS garantit l'idempotence : on peut relancer le seed sans erreur.

Requêtes synchrones

better-sqlite3 est synchrone par design — pas de await, pas de callbacks.

// Lecture
const files = db.prepare('SELECT * FROM file_metadata WHERE folder = ?').all(folder)
 
// Écriture
const result = db.prepare(
  'INSERT INTO file_metadata (filename, original_name, folder, size, mime_type) VALUES (?, ?, ?, ?, ?)'
).run(filename, originalName, folder, size, mimeType)
 
// Transaction
const transfer = db.transaction((fileId: number) => {
  const file = db.prepare('SELECT * FROM file_metadata WHERE id = ?').get(fileId)
  db.prepare('INSERT INTO trash ...').run(...)
  db.prepare('DELETE FROM file_metadata WHERE id = ?').run(fileId)
})
transfer(42)

Les transactions sont atomiques — si une étape échoue, tout est annulé.

Précédent
Authentification avec Better Auth
Suivant
Système d'upload de fichiers

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main