Headless CMS open source — léger, rapide, sans dépendances lourdes.
Modèlez votre contenu, gérez vos médias, consommez votre API REST depuis n'importe quelle application.
Mutane est un CMS headless écrit entièrement en Go. Il expose une API REST publique sécurisée par clé API et une interface d'administration web complète, le tout dans un seul binaire autonome.
- Zéro framework — stdlib Go +
net/http - Interface admin intégrée — HTML/CSS/JS vanilla, aucun build frontend requis
- Base de données — PostgreSQL, migrations automatiques au démarrage
- Binaire unique — déployez un seul fichier, aucune dépendance runtime
- ✅ Types de contenu dynamiques avec builder visuel de champs
- ✅ 13 types de champs : texte, nombre, email, date, booléen, rich text, JSON, média, énumération, relation, UID, password, blocks
- ✅ Réorganisation des champs par glisser-déposer
- ✅ Entrées avec statut brouillon / publié
- ✅ Pagination, recherche et filtres sur tous les tableaux
- ✅ Upload de fichiers (32 MB max par fichier)
- ✅ Galerie avec prévisualisation
- ✅ Stockage local avec URL publique configurable
- ✅ Authentification JWT (Bearer token + cookie de session)
- ✅ 2FA TOTP (Google Authenticator, Authy…)
- ✅ Clés API publiques (
mut_pub_…) et privées (mut_prv_…) - ✅ Rotation de clé atomique (révocation + régénération en une transaction)
- ✅ Expiration de clé configurable (30j / 90j / 6 mois / 1 an / jamais)
- ✅ Hash SHA-256 des clés (O(1), sans bcrypt pour les tokens haute entropie)
- ✅ Hot-reload en mode dev (air + SSE browser refresh)
- ✅ API publique versionnée
/v1/avec pagination et enveloppe JSON standardisée - ✅ CORS configuré
- ✅ Migrations SQL auto-appliquées au démarrage
| Outil | Version minimale |
|---|---|
| Go | 1.22 |
| PostgreSQL | 14 |
| Node.js (optionnel, pour rebuilder le CSS) | 18 |
git clone https://github.com/votre-org/mutane.git
cd mutanecp .env.example .envÉditez .env :
PORT=8080
APP_ENV=development # active le hot-reload
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=votre_mot_de_passe
DB_NAME=mutane
DB_SSLMODE=disable
JWT_SECRET=changez-moi-avec-une-chaine-aleatoire-min-32-chars
UPLOAD_DIR=uploads
UPLOAD_BASE_URL=http://localhost:8080/uploadspsql -U postgres -c "CREATE DATABASE mutane;"Les migrations s'appliquent automatiquement au premier démarrage.
Mode développement (hot-reload) :
# Installer air (une seule fois)
go install github.com/air-verse/air@latest
# Lancer
airMode standard :
go run ./cmd/server/Build production :
go build -o mutane ./cmd/server/
./mutaneOuvrez http://localhost:8080 — vous serez redirigé vers la page de configuration initiale pour créer votre compte administrateur.
mutane/
├── cmd/
│ └── server/
│ └── main.go # Point d'entrée
│
├── internal/
│ ├── api/
│ │ ├── handler.go # Agrégation des handlers
│ │ ├── middleware.go # Auth (Session, Bearer, APIKey), CORS, Logger
│ │ └── routes.go # Table de routage complète
│ │
│ ├── auth/
│ │ ├── handler.go # Login, Register, Logout, 2FA
│ │ ├── jwt.go # Génération / validation JWT
│ │ └── totp.go # Secret TOTP, validation
│ │
│ ├── content/
│ │ ├── model.go # ContentType, Field, Entry + FieldType constants
│ │ ├── repository.go # CRUD SQL (LEFT JOIN fields, ReorderFields…)
│ │ └── handler.go # Handlers HTTP + pages admin
│ │
│ ├── apikey/
│ │ ├── model.go # APIKey, KeyType (public/private)
│ │ ├── repository.go # Create, Rotate, Revoke, Validate (SHA-256)
│ │ └── handler.go # Endpoints CRUD + Rotate
│ │
│ ├── media/
│ │ ├── model.go # Media struct
│ │ ├── repository.go # CRUD table media
│ │ ├── storage.go # LocalStorage (save, delete, URL)
│ │ └── handler.go # Upload, List, Delete
│ │
│ ├── admin/
│ │ ├── handler.go # Me, SettingsPage
│ │ └── stats.go # Dashboard stats
│ │
│ ├── public/
│ │ └── handler.go # GET /v1/{slug}, /v1/{slug}/{id}, /v1/media
│ │
│ ├── setup/
│ │ └── handler.go # Onboarding initial
│ │
│ ├── devreload/
│ │ └── reload.go # SSE hub + polling watcher (dev only)
│ │
│ ├── ctxkey/
│ │ └── ctxkey.go # Clé de contexte partagée (userID)
│ │
│ └── database/
│ ├── connect.go # Pool de connexion PostgreSQL
│ └── migrate.go # Runner de migrations SQL
│
├── migrations/
│ ├── 001_users.up.sql
│ ├── 002_content_types.up.sql
│ ├── 003_entries.up.sql
│ ├── 004_media.up.sql
│ ├── 005_api_keys.up.sql
│ ├── 006_api_keys_v2.up.sql
│ └── 007_api_keys_fix.up.sql
│
├── web/
│ └── static/
│ ├── admin.html # Interface admin (SPA vanilla JS)
│ └── login.html # Page de connexion
│
├── uploads/ # Fichiers uploadés (gitignored)
├── .air.toml # Config hot-reload (air)
├── .env.example
├── go.mod
└── go.sum
Toutes les routes /api/* requièrent un header Authorization: Bearer <token>.
Les routes /v1/* requièrent un header X-API-Key: mut_pub_xxxx ou X-API-Key: mut_prv_xxxx.
| Méthode | Endpoint | Description |
|---|---|---|
POST |
/api/auth/register |
Créer un compte |
POST |
/api/auth/login |
Connexion → retourne token |
POST |
/api/auth/logout |
Déconnexion (clear cookie) |
POST |
/api/auth/2fa/enable |
Activer la 2FA (Bearer) |
POST |
/api/auth/2fa/verify |
Vérifier le code TOTP (Bearer) |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/content-types |
Liste tous les types |
POST |
/api/content-types |
Créer un type |
GET |
/api/content-types/{id} |
Détail + champs |
PUT |
/api/content-types/{id} |
Modifier |
DELETE |
/api/content-types/{id} |
Supprimer |
POST |
/api/content-types/{id}/fields |
Ajouter un champ |
DELETE |
/api/content-types/{id}/fields/{fid} |
Supprimer un champ |
PUT |
/api/content-types/{id}/fields/reorder |
Réordonner les champs |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/content-types/{typeId}/entries |
Liste les entrées |
POST |
/api/content-types/{typeId}/entries |
Créer une entrée |
GET |
/api/content-types/{typeId}/entries/{id} |
Détail |
PUT |
/api/content-types/{typeId}/entries/{id} |
Modifier |
DELETE |
/api/content-types/{typeId}/entries/{id} |
Supprimer |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/media |
Liste les médias |
POST |
/api/media/upload |
Uploader un fichier (multipart) |
DELETE |
/api/media/{id} |
Supprimer |
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/keys |
Liste toutes les clés (actives + révoquées) |
POST |
/api/keys |
Créer une clé |
POST |
/api/keys/{id}/rotate |
Rotation (révoque + régénère) |
DELETE |
/api/keys/{id} |
Révoquer |
| Méthode | Endpoint | Query params | Description |
|---|---|---|---|
GET |
/v1/{slug} |
page, limit |
Liste les entrées publiées |
GET |
/v1/{slug}/{id} |
— | Détail d'une entrée publiée |
GET |
/v1/media |
page, limit |
Liste les médias |
Exemple de réponse paginée :
{
"data": [
{ "id": 1, "title": "Hello World", "published_at": "2025-01-15T10:00:00Z" }
],
"meta": {
"total": 42,
"page": 1,
"limit": 20
}
}Exemple d'appel curl :
curl https://votre-domaine.com/v1/articles \
-H "X-API-Key: mut_pub_a1b2c3d4e5f6" \
| jq .| Type | Préfixe | Usage recommandé |
|---|---|---|
| Publique | mut_pub_… |
Applications front-end, navigateur, mobile |
| Privée | mut_prv_… |
Server-to-server, scripts back-end, CI/CD |
⚠️ Ne jamais exposer une clé privée côté client. Elle doit rester dans les variables d'environnement de votre serveur.
Les clés sont stockées sous forme de hash SHA-256 (jamais en clair). La valeur complète n'est affichée qu'une seule fois à la création ou après une rotation.
Avec APP_ENV=development et air :
Sauvegarde d'un .go → air recompile → serveur redémarre → navigateur recharge
Sauvegarde admin.html → watcher SSE détecte → navigateur recharge (~350ms)
En production (APP_ENV absent ou différent de development) : aucun watcher, aucune route /dev/reload, aucun overhead.
| Variable | Défaut | Description |
|---|---|---|
PORT |
8080 |
Port d'écoute du serveur |
APP_ENV |
— | development pour activer le hot-reload |
DB_HOST |
localhost |
Hôte PostgreSQL |
DB_PORT |
5432 |
Port PostgreSQL |
DB_USER |
— | Utilisateur PostgreSQL |
DB_PASSWORD |
— | Mot de passe PostgreSQL |
DB_NAME |
— | Nom de la base de données |
DB_SSLMODE |
disable |
Mode SSL (disable, require, verify-full) |
DATABASE_URL |
— | DSN complète (prioritaire sur les variables ci-dessus) |
JWT_SECRET |
change-me |
Secret de signature JWT (min. 32 caractères) |
UPLOAD_DIR |
uploads |
Répertoire de stockage des fichiers |
UPLOAD_BASE_URL |
— | URL de base pour les médias servis |
docker build -t mutane .
docker run -p 8080:8080 --env-file .env.prod mutanedocker compose up -d# Build
go build -ldflags="-s -w" -o mutane ./cmd/server/
# Copier le binaire + dossiers requis sur le serveur
scp mutane user@server:/opt/mutane/
scp -r migrations web user@server:/opt/mutane/
# Lancer (avec systemd, supervisor, etc.)
APP_ENV=production ./mutanego test ./...npm install
npm run css:watchCréez un fichier migrations/00X_description.up.sql — il sera appliqué automatiquement au prochain démarrage.
- Webhooks sur événements (création/modification/suppression d'entrées)
- Internationalisation (i18n) des entrées
- Rôles et permissions granulaires
- Plugins / champs personnalisés
- Interface de gestion des utilisateurs
- Export/import de contenu (JSON, CSV)
- Templates Templ pour le rendu server-side
MIT — voir LICENSE