Modèle de données Firestore

L'image complète de ce qui est stocké où dans Firestore en mode Firebase. En mode SQLite, les mêmes shapes sont stockées dans des tables équivalentes (avec colonnes JSON pour les shapes imbriqués) —

L'image complète de ce qui est stocké où dans Firestore en mode Firebase. En mode SQLite, les mêmes shapes sont stockées dans des tables équivalentes (avec colonnes JSON pour les shapes imbriqués) — voir Modèle de données SQLite pour la traduction.

Collections

firestore/
├── posts/{postId}              — posts ET pages (distingués par `type`)
├── terms/{termId}              — catégories ET tags (distingués par `type`)
├── media/{mediaId}             — métadonnées des images uploadées
├── users/{uid}                 — profils admin/éditeur + préférences
├── settings/                   — documents singletons
│   ├── site                    — réglages site (titre, thème, plugins, menus, …)
│   └── externalRegistry        — liste des plugins/thèmes externes installés
└── config/                     — singletons de config
    ├── flexweg                 — clé API Flexweg (lecture admin uniquement)
    └── admin                   — sondage règles pour le bootstrap admin (existence check)

posts/{postId}

Le même shape couvre posts et pages.

TS
{
  id: string;                          // = postId
  type: "post" | "page";
  title: string;
  slug: string;
  contentMarkdown: string;
  excerpt?: string;
  heroMediaId?: string;
  authorId: string;                    // uid User
  primaryTermId?: string;              // catégorie principale (uniquement posts)
  termIds: string[];                   // catégories + tags (uniquement posts)
  status: "draft" | "online";
  publishedAt?: Timestamp;
  createdAt: Timestamp;
  updatedAt: Timestamp;

  // Tracking de publication
  lastPublishedPath?: string;
  lastPublishedHash?: string;
  previousPublishedPaths?: string[];   // chemins dont la suppression a échoué
  lastPublishedPathsByLocale?: Record<string, string>;   // multilang

  // SEO
  seo?: { title?: string; description?: string; ogImage?: string };

  // Données opaques de plugin (API ≥ 1.2)
  translations?: Record<string, unknown>;   // ex. { fr: { slug, title, contentMarkdown, excerpt, seo } }
}

Conventions de Timestamp

En mode Firebase, Timestamp est firebase.firestore.Timestamp. En mode SQLite, c'est un objet équivalent avec toMillis() et toDate().

Tri de listing : postSortMillis(post) = publishedAt → updatedAt → createdAt → maintenant.

terms/{termId}

TS
{
  id: string;
  type: "category" | "tag";
  name: string;
  slug: string;
  description?: string;
  parentId?: string;                   // catégories uniquement
  seo?: { title?: string; description?: string; ogImage?: string };
  translations?: Record<string, unknown>;   // multilang : { fr: { name, slug, description, seo } }
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

media/{mediaId}

TS
{
  id: string;
  fileName: string;
  alt?: string;
  caption?: string;
  formats?: Record<string, {            // pipeline multi-variantes
    url: string;
    width: number;
    height: number;
    bytes?: number;
    format: string;                     // "webp", "svg", …
  }>;
  defaultFormat?: string;
  url?: string;                         // legacy single-URL (anciens uploads)
  size?: number;
  contentType?: string;
  uploadedBy?: string;                  // uid User
  createdAt: Timestamp;
}

users/{uid}

Le {uid} correspond à auth.currentUser.uid (Firebase) ou à l'id généré côté serveur (SQLite).

TS
{
  uid: string;                          // = clé du document
  email: string;
  firstName?: string;
  lastName?: string;
  bio?: string;
  avatarMediaId?: string;
  role: "admin" | "editor";
  preferences?: { adminLocale?: string };
  createdAt: Timestamp;
}

settings/site

Singleton qui contient toute la config site-wide.

TS
{
  title: string;
  description: string;
  language: string;                     // BCP-47
  baseUrl: string;
  activeThemeId: string;
  homeMode: "latest" | "static-page";
  homePageId?: string;
  paginationMode: "global" | "paginated";
  enabledPlugins: string[];             // ids de plugins activés
  menus: {
    header: MenuItem[];
    footer: MenuItem[];
  };
  themeConfigs?: Record<string, unknown>;   // config par thème (survit aux changements de thème)
  pluginConfigs?: Record<string, unknown>;  // config par plugin (survit aux désinstallations)
  updatedAt: Timestamp;
}

interface MenuItem {
  id: string;
  type: "custom" | "page" | "category";
  label: string;
  pageId?: string;                      // pour type "page"
  termId?: string;                      // pour type "category"
  url?: string;                          // pour type "custom"
  children?: MenuItem[];
  translations?: Record<string, { label: string }>;
}

settings/externalRegistry

Liste des plugins / thèmes externes installés. Migré depuis l'ancien dist/admin/external.json au premier boot après installation.

TS
{
  plugins: ExternalEntry[];
  themes: ExternalEntry[];
}

interface ExternalEntry {
  id: string;
  version: string;
  apiVersion: string;
  entryPath: string;                    // ex. "plugins/my-plugin/bundle.js"
}

config/flexweg

TS
{
  apiKey: string;                       // clé API Flexweg
  siteUrl: string;
  apiBaseUrl: string;
}

Lecture restreinte aux admins via les règles Firestore. Source de vérité pour l'API Files.

config/admin

Un document vide utilisé comme sonde de règle Firestore pour détecter le bootstrap admin. Les règles autorisent la lecture si et seulement si isBootstrapAdmin() est vrai (signed in + email verified + matchant l'email épinglé). L'admin call getDoc(config/admin) et utilise le succès / échec comme indicateur.

Indexes composites (mode paginated)

Deux index requis pour les requêtes paginées :

  • posts: type ASC, createdAt DESC
  • posts: type ASC, status ASC, createdAt DESC

Voir Réglages de performance pour le détail.

Coût Firestore typique

Pour un site avec 100 posts en mode global :

  • Boot : 1 souscription qui lit 100 docs (5 Ko de transit). Puis 0 lecture supplémentaire jusqu'à un changement.
  • Publication : 0-5 reads par publication (pour résoudre primaryTerm, etc.)
  • Total mensuel : largement sous les 50k reads/jour du plan Spark gratuit

Pour 5000 posts en mode paginated :

  • Boot : ~25 reads pour la première page (limit 25)
  • Pagination : 25 reads par page suivante
  • Counts : 1 read par agrégation (les counts en mode global sont gratuits in-memory)
  • Total mensuel : dépend de l'activité éditeur

Migration vers SQLite

Si vous voulez basculer de Firebase à SQLite :

  1. Exportez via Réglages → Backend de données → Exporter (produit un JSON avec tout)
  2. Réinstallez l'admin en mode SQLite via le formulaire d'installation
  3. Importez le JSON via Réglages → Backend de données → Importer

Les Timestamps sont convertis automatiquement entre les deux formats lors de l'import.