Des listes de cartes, et les propriétés que votre équipe déclare.
Un espace contient des listes. Une carte a une clé lisible, un statut, une place dans une hiérarchie — puis les propriétés que votre équipe a déclarées, dans un registre unique partagé par toutes les listes. Pas d’assigné ni d’échéance imposés d’office à une base de contenu ; pas dix copies de « Priorité » qui divergent.
Une vue enregistrée, c’est un filtre, un tri et un regroupement — une donnée, pas une requête que quelqu’un a tapée. Le tableau kanban ci-dessous et la table qui suit sont la même définition, vue de deux façons.

- propriétés
- Dix types, réutilisés d’une liste à l’autre : texte, nombre, select, multi-select, date, case à cocher, url, personne, plusieurs personnes, fichier. Validées à l’écriture : une valeur mal typée est refusée à l’entrée au lieu de casser plus tard toutes les vues qui trient dessus.
- corps de page
- Titres, listes, to-dos, citations, encadrés, code, séparateurs et images. Les marques sont nommées — gras, italique, code, barré, lien — donc le format ne peut pas transporter de HTML et il n’y a rien à assainir.
- hiérarchie
- Les cartes s’imbriquent. Un sous-arbre ou les ancêtres d’une carte, c’est une requête, et déplacer une carte déplace son sous-arbre au lieu de l’orpheliner. Une carte peut figurer dans plusieurs listes sans quitter celle où elle est née.
- fichiers
- Les pièces jointes et les blocs image portent un id de fichier, jamais une URL. L’API sert les octets depuis une route à portée limitée : pas de bucket public, pas de lien signé à transmettre par erreur.


Votre site lit ce que votre équipe vient d’écrire.
Aucune étape de publication qui copie le contenu ailleurs, aucun second stockage à garder synchronisé. Chaque écran de l’app passe par la même API que celle dont vous obtenez une clé — l’API ne peut donc pas prendre de retard sur le produit, puisqu’elle est le produit.
La spec est générée à partir des routes annotées et commitée avec elles. Générez vos propres types à partir d’elle, et une propriété renommée devient une erreur de build chez vous, pas une section vide en production.
- protocole
- REST — et jamais GraphQL. C’est tranché, pas en suspens.
- spec
- OpenAPI sur /openapi.json, avec une référence interactive sur /api/docs.
- identifiant
- une clé d’API par espace, avec son propre rôle — viewer lit, member écrit. Révocables une par une, dernière utilisation datée.
- pagination
- des curseurs keyset partout. Pas d’offset, donc pas de page qui glisse sous vos pieds.
- isolation
- la row-level security de Postgres sous chaque requête. Un espace dont vous n’êtes pas membre répond 404, pas 403.
npx openapi-typescript https://cms.spunto.net/openapi.json \
--default-non-nullable false -o cms.d.tsimport createClient from "openapi-fetch"
import type { paths } from "./cms"
const api = createClient<paths>({
baseUrl: "https://cms.spunto.net/api",
// an API key: spk_…, scoped to one workspace
headers: { Authorization: `Bearer ${SPUNTO_CMS_TOKEN}` },
})
// A view is data: a filter tree, sorts, an optional group-by.
const query = "/workspaces/{workspaceId}/collections/{collectionId}/query"
const { data } = await api.POST(query, {
params: { path: { workspaceId, collectionId } },
body: {
definition: {
filter: { op: "eq", field: { fieldId: STAGE }, value: "published" },
sort: [
{ field: { fieldId: "sys:created_at" }, direction: "desc" },
],
},
limit: 20,
},
})
data?.rows // "DOC-12", its title, its typed properties
data?.nextCursor // keyset — there is no page number to ask forSTAGE est l’id d’un select déclaré par votre équipe — seuls le titre, le corps et le statut sont intégrés. Le parcours complet, d’un compte vide jusqu’à cet appel, est dans le quickstart.
Un script n’est qu’un membre de plus.
Le contenu n’a pas à être tapé à la main. Un script ou un agent crée des cartes, renseigne des propriétés typées, écrit un corps structuré et lance une mise à jour en masse sur une vue — par les mêmes routes et avec les mêmes contrôles que la session d’une personne.
Et quoi qu’ils écrivent, autre chose peut suivre. Chaque modification s’ajoute au journal d’événements de l’espace dans la transaction qui l’a faite : un événement existe si et seulement si la modification a été commitée.
- journal
- qui a archivé cette carte, ce qui a bougé cette semaine — lisible par curseur, donc un consommateur tombé en panne rattrape son retard au lieu de perdre des événements
- webhooks
- déclenchés depuis ce journal, avec un historique des livraisons et une reprise après échec
- apps
- des apps OAuth installables : un tiers a sa propre identité, bornée par un rôle et les scopes que l’espace lui a accordés
curl -X POST "$CMS/api/workspaces/$WS/collections/$LIST/items" \
-H "Authorization: Bearer $SPUNTO_CMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Release notes — v0.8",
"properties": { "stage": "draft", "tags": ["release"] },
"blocks": { "blocks": [
{ "type": "heading", "level": 2,
"text": [{ "text": "What changed" }] },
{ "type": "bulleted",
"text": [{ "text": "Keyset pagination", "bold": true }] },
{ "type": "todo", "checked": false,
"text": [{ "text": "Screenshot the board" }] }
] }
}'$CMS vaut https://cms.spunto.net ; stage et tags sont des clés de votre registre. Une valeur qui ne peut pas être lue comme le type déclaré est refusée ici, à l’écriture, plutôt que de casser plus tard toutes les vues qui trient dessus.
Fait pour remplacer Notion et ClickUp.
Pas encore — voici l’écart.
Le but : l’outil où une équipe planifie et écrit toute la journée, dont le contenu est aussi une donnée qu’un site et un agent peuvent lire. Une page qui ne liste que des fonctionnalités fait perdre son temps à qui la croit : l’autre liste est donc juste à côté — reprise de la propre page des limites du produit.
déjà là
- vues
- tableau, liste et kanban sur la même définition enregistrée — champs affichés par vue, colonnes dans votre ordre, réordonnables au glisser
- cartes
- une clé lisible, un statut, des sous-tâches à toute profondeur, plusieurs assignés, une icône, et un corps en blocs avec un menu slash
- navigation
- une barre latérale de listes et de groupes qui persiste entre les pages, et ⌘K sur les titres de cartes et les noms de listes
- import
- une base Notion devient une liste en une commande — rejouable : la deuxième exécution n’écrit rien
- connexion
- e-mail et mot de passe ou Google, des rôles par espace (owner, admin, member, viewer), des invitations
pas encore
- calendrier
- l’API et le compilateur de vues le servent ; l’app web n’a pas encore d’écran pour lui
- recherche
- titres et noms de listes seulement — pas d’index plein texte sur le corps des cartes
- publication
- pas de séparation brouillon / publié — une propriété select sur laquelle filtre votre site est la réponse honnête aujourd’hui
- pages de doc
- chaque page est une carte dans une liste ; pas encore d’arborescence de wiki autonome
- langue
- l’interface de l’app n’existe qu’en français ; l’API et sa documentation sont en anglais
- mobile
- un client iOS natif vit dans le dépôt et parle à la même spec — pas sur l’App Store