Spunto CMSun espace de travail avec une API

Un espace de travail pour écrire.
Une API qui le sert.

La plupart des équipes finissent par faire tourner deux choses : un endroit agréable où écrire et organiser le travail, et une API de contenu assez propre pour alimenter un site ou un agent. Puis elles passent un an à les synchroniser. Spunto CMS est une seule chose — des listes de cartes, des propriétés typées, des pages en blocs et des vues enregistrées d’un côté ; les mêmes lignes en REST de l’autre. Pas un export. Les mêmes lignes.

tableau · liste · kanbanpages en blocsREST + OpenAPIaccessible en écriture aux agents
une requête · n’importe quelle clé d’espace
$ curl https://cms.spunto.net/api/workspaces \
  -H "Authorization: Bearer spk_…"

une clé d’API depuis les réglages de votre espace — ou laissez l’API et inscrivez-vous sur cms.spunto.net

01 — L’espace de travail

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.

Spunto CMS : la liste Sprint 42 en kanban par statut — À faire, En cours, Terminé — avec des cartes montrant priorité, effort, équipe, tags et assigné
la vraie app, sur ses données de démo, en français — un kanban par statut, et quatre autres vues enregistrées à un onglet de là
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.
La même liste en table triée par priorité, avec les clés, les titres, les sous-tâches indentées sous leur parent, les statuts et les colonnes typées
la même liste, en table — les sous-tâches sous leur parent
Une carte ouverte dans le panneau latéral : un corps en blocs avec puces, to-dos, une citation, un encadré et un bloc de code SQL, les sous-tâches dessous, et les propriétés typées à droite
une carte : un document en blocs, des sous-tâches, et les propriétés du registre
02 — La moitié headless

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.
une fois — les types depuis la spec en ligne
npx openapi-typescript https://cms.spunto.net/openapi.json \
  --default-non-nullable false -o cms.d.ts
read-published.ts
import 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 for

STAGE 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.

03 — Écrit aussi par des agents

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
create-card.sh
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.

04 — Où il va

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

↳ aussi chez Spunto