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.
{
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}
{
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}
{
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).
{
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.
{
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.
{
plugins: ExternalEntry[];
themes: ExternalEntry[];
}
interface ExternalEntry {
id: string;
version: string;
apiVersion: string;
entryPath: string; // ex. "plugins/my-plugin/bundle.js"
}
config/flexweg
{
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 DESCposts: 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 :
- Exportez via Réglages → Backend de données → Exporter (produit un JSON avec tout)
- Réinstallez l'admin en mode SQLite via le formulaire d'installation
- Importez le JSON via Réglages → Backend de données → Importer
Les Timestamps sont convertis automatiquement entre les deux formats lors de l'import.