Spunto CMS
Concepts

Espaces de travail et membres

La frontière entre tenants — ce que contient un espace de travail, ce que permettent les quatre rôles, et pourquoi un inconnu reçoit 404 plutôt que 403.

Un espace de travail est l'unité d'isolation. Tout le reste vit dans un espace et un seul : listes, cartes, registre des propriétés, statuts, fichiers, clés API, webhooks et journal d'événements.

Rien ne le traverse. Aucune requête du produit ne lit deux espaces à la fois, et c'est garanti sous l'application : chaque requête s'exécute sur une connexion Postgres limitée à un seul tenant par la sécurité au niveau des lignes (RLS), et l'API s'y connecte avec un rôle qui n'a aucun moyen de la contourner. Une clause WHERE oubliée ne peut pas laisser fuir les lignes d'un autre tenant, parce que ce n'est pas elle qui tient la barrière.

Une conséquence visible

Le sélecteur d'espaces n'affiche aucun compteur. Pas faute d'en avoir voulu : compter les cartes de tous vos espaces demanderait une transaction par espace, et l'isolation qui rend le produit sûr est celle-là même qui rend cet agrégat coûteux. Les compteurs vivent un niveau plus bas, sur l'écran d'accueil de chaque espace.

Membres et rôles

On entre dans un espace de travail en en devenant membre. Il existe quatre rôles, et ils sont ordonnés — une route qui exige member accepte aussi admin et owner :

RôlePeut
viewerLire le contenu et les vues
memberTout ce qui précède, plus créer et modifier des cartes
adminTout ce qui précède, plus le registre, les statuts, les membres, les clés API et les invitations
ownerTout ce qui précède, plus l'espace de travail lui-même

Les rôles sont vérifiés à chaque écriture, côté serveur. Un client connaît son propre rôle pour pouvoir masquer les boutons voués à l'échec, mais masquer un bouton est une politesse, pas un contrôle.

Pourquoi vous recevez 404, et non 403

Demandez un espace de travail dont vous n'êtes pas membre : l'API répond 404 Not Found, jamais 403 Forbidden.

C'est voulu. Un 403 confirmerait que l'espace existe — précisément ce que cherche à savoir quelqu'un qui énumère des identifiants. Il en va de même pour une section de réglages que votre rôle ne peut pas ouvrir : invisible, donc inexistante.

Un 404 de cette API veut donc dire l'une de deux choses — la ressource n'existe pas, ou elle n'est pas à vous — et l'API ne vous dira pas laquelle.

Le périmètre vient de l'appartenance, jamais de l'URL

Mettre l'identifiant d'un espace dans un chemin n'accorde rien. Toute requête sous /workspaces/{workspaceId}/… franchit deux portes avant d'atteindre une route : l'authentification (401 sans bearer valide) et l'appartenance (404 sans elle).

Les deux sont montées sur le préfixe du chemin plutôt que route par route : une route ajoutée demain est protégée le jour même où elle est écrite. La sécurité au niveau des lignes forme un second plancher en dessous — elle empêche de traverser d'un tenant à l'autre, mais c'est la vérification d'appartenance qui décide si vous avez le droit de nommer celui-ci.

Deux sortes de porteurs

Deux types d'identifiants passent la porte d'entrée, et au-delà rien ne permet de les distinguer :

  • une session, ouverte par un humain qui se connecte — elle porte l'accès de cette personne à tous ses espaces, et expire au bout de 30 jours ;
  • une clé API (spk_…), pour les logiciels — limitée à un espace de travail, avec un rôle fixé à l'émission, révocable seule.

Pour tout ce qui tourne sans surveillance, utilisez une clé. Une session est le mauvais identifiant pour un script : elle est plus large que ce que la tâche demande, on ne peut pas la révoquer sans déconnecter la personne partout, et elle finira par expirer en pleine nuit.

Il en existe un troisième : le jeton d'installation d'app (spa_…), émis par OAuth pour les logiciels tiers. Celui-là porte en plus des scopes.

Vue d'ensemble

Chaque espace de travail a un écran d'accueil servi par une seule route, GET /workspaces/{workspaceId}/overview : le nombre de cartes dans chaque catégorie de statut, l'activité des quatorze derniers jours et l'avancement par liste.

C'est une route à part entière plutôt que l'effet de bord d'une autre lecture, parce que le comptage est ce qui casse en premier dans un système de contenu qui grossit. Le tenir à part rend son coût visible et assumé — trois requêtes, quel que soit le nombre de listes — au lieu de le cacher dans un chargement de page qui, autrefois, ne coûtait rien.

Sur cette page