Synaplan
기업을 위한 오픈소스 AI 플랫폼
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Repository for the scopguard app
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Le portail de validation client qui transforme les demandes hors perimetre en decisions facturees.
État du produit. Kora est en ligne pour des pilotes (Vercel à Francfort, Supabase, Azure, Resend). Les sections qui suivent décrivent le positionnement et le MVP visé ; l'état réel de chaque fonctionnalité est suivi dans PERSONAS_USER_STORIES.md. La pile et la sécurité sont décrites dans TECHNOLOGIES.md, les règles pour les agents IA dans AGENTS.md.
Kora est un SaaS destine aux agences web, studios de design et consultants creatifs. Il centralise les retours clients sur des livrables visuels et aide les equipes a transformer chaque demande en une decision claire : incluse dans le perimetre, modification mineure, ou prestation supplementaire a chiffrer.
Le produit ne cherche pas a etre un simple outil de commentaires sur PDF. Son objectif est de proteger la marge des agences, accelerer les validations et fournir une preuve exploitable de ce qui a ete demande, accepte et livre.
Les agences produisent des maquettes, identites visuelles, sites web, landing pages, rapports et documents marketing. Leur validation client est souvent dispersee entre :
Cette fragmentation rend les decisions ambigues. Un retour tel que "ajouter une section" peut etre traite comme une petite correction, alors qu'il implique parfois plusieurs jours de conception, developpement et validation supplementaires.
Les consequences sont concretes :
Kora place la discussion directement sur le livrable et structure le parcours de decision.
Inclus, Hors scope ou À clarifier.Chaque retour devient ainsi soit une action prevue, soit une prestation supplementaire acceptee. Le travail additionnel ne reste plus cache dans une chaine d'e-mails.
Pour les agences : recuperer du temps facturable et reduire les litiges de perimetre.
Pour les clients : donner un retour clair et approuver un livrable sans naviguer dans un outil complexe.
Pour les chefs de projet : obtenir un historique unique qui relie commentaire, arbitrage, devis, approbation et livraison.
La promesse produit est simple :
Chaque retour client devient une decision claire, et chaque travail supplementaire peut etre accepte avant d'etre realise.
Le produit commencera par les petites et moyennes agences qui realisent des livrables visuels et travaillent au forfait.
| Segment | Situation | Valeur apportee |
|---|---|---|
| Agences web | Retours repetes sur wireframes, maquettes et recettes de sites | Encadrer les revisions et chiffrer les nouvelles demandes |
| Studios de branding | Multiples allers-retours sur logos, identites et chartes | Tracer les validations de versions et limiter les cycles |
| Agences marketing | Validations de landing pages, publicites, rapports et campagnes | Accelérer les approbations et rendre les changements visibles |
| Freelances premium | Peu de temps administratif et risque eleve de travail gratuit | Formaliser les decisions sans imposer un outil lourd au client |
Le premier segment recommande est celui des agences web de 3 a 30 personnes. Elles ont des projets suffisamment structures pour souffrir du hors perimetre, mais n'ont pas toujours les outils ou procedures des grandes entreprises.
Kora est un systeme de controle du perimetre client pour agences creatrices de livrables visuels.
La validation visuelle est le point d'entree. La valeur distinctive est la transformation d'un retour client en decision operationnelle et commerciale avant que le travail soit engage.
Ce positionnement evite deux erreurs courantes :
| Alternative | Ce qu'elle fait bien | Limite pour notre cas | Difference Kora |
|---|---|---|---|
| Filestage / Ziflow | Relecture, commentaires et circuits de validation de contenu | Oriente production et conformite ; le lien avec le hors perimetre et le devis reste secondaire | Qualification native des demandes et creation de propositions supplementaires |
| Frame.io | Revue video avancee : plages de temps, dessin, image par image, comparaison de versions | Optimise surtout les workflows de production video | La video y est commentee (moment et endroit, US-052), au service du perimetre commercial, comme les PDF, les maquettes et les sites |
| Marker.io / BugHerd | Retours poses sur le site, contexte technique (navigateur, console), envoi vers Jira, Asana, Linear, Slack | Le retour part en ticket sans etre qualifie ni chiffre | Chaque retour est qualifie, chiffre si besoin et accepte par le client avant d'etre execute |
| MarkUp.io | Commentaires visuels rapides et simples | Peu de mecanismes pour transformer un retour en accord commercial | Du commentaire a la proposition acceptee, avec une piste d'audit |
| ClickUp / Asana / Trello | Taches, planification et travail interne | L'experience client est trop lourde et le contexte visuel est limite | Un espace client focalise sur une decision par livrable |
| CRM / devis / facturation | Devis, contrats, factures et suivi commercial | Le contexte du retour se perd avant le devis | Le devis complementaire part du commentaire visuel exact qui l'a motive |
| E-mail et messageries | Universels et immediats | Decisions dispersees, aucune traçabilite fiable, pas de garde-fou commercial | Un historique unique, structure et exportable |
La concurrence valide l'existence du besoin de revue et d'approbation. Kora ne cherche pas a gagner par un plus grand catalogue de fonctionnalites. Il gagne par une question plus proche de la marge : cette demande etait-elle incluse, et le client a-t-il accepte de payer le supplement avant execution ?
Le MVP doit permettre de tester ce positionnement avec un flux complet, sans construire un outil de production generaliste.
MAX_VIDEO_UPLOAD_MB, 1 Go par défaut).Demander des modifications (regle serveur faite, bouton a ajouter cote client) et Approuver la version.incluse, modification mineure, hors perimetre, a clarifier (realise : « Inclus », « Hors scope » et « À clarifier »).Les elements suivants sont utiles mais ne doivent pas retarder la validation du besoin principal :
Une integration Stripe, puis des connecteurs vers les outils deja utilises par les agences, seront abordes une fois que le parcours commentaire -> devis complementaire -> acceptation sera valide.
Kora est un produit B2B par abonnement. Sa valeur est liee au chiffre d'affaires protege et au temps administratif evite, et non au volume brut de fichiers importes.
| Offre | Prix indicatif | Inclut |
|---|---|---|
| Solo | 19 USD / mois | 1 utilisateur, projets et liens client limites |
| Studio | 49 USD / mois | Petite equipe, projets actifs et historique de validation |
| Agence | 119 USD / mois | Equipe, branding, propositions supplementaires et exports |
| White-label | A partir de 299 USD / mois | Domaine personnalise, marque blanche, controles avances et support prioritaire |
Les leviers de monétisation complementaires peuvent inclure les utilisateurs supplementaires, un volume de projets actifs plus eleve, la conservation longue duree des preuves et les integrations de facturation.
Le projet repose sur des hypotheses explicites a tester avant un investissement produit important :
Avant de construire un ensemble large de fonctionnalites, mener des entretiens avec 15 a 20 dirigeants d'agences web et studios de design.
Questions de recherche :
Le signal le plus fort n'est pas une reponse positive abstraite. C'est une agence qui accepte de tester le flux sur un projet en cours, partage ses procedures actuelles ou souhaite payer pour eviter une perte de marge recurrente.
Le produit doit demontrer un effet concret sur le flux commercial des agences.
A terme, Kora doit devenir la source de verite des decisions client sur les projets au forfait : le lieu ou une demande est contextualisee, arbitree, chiffree, acceptee et rattachee a une livraison.
Le produit ne remplace pas un outil de gestion de projet, un CRM ou une solution comptable. Il les relie au moment le plus fragile de la relation agence-client : celui ou une remarque apparemment simple peut se transformer en travail gratuit.
Les données sont enregistrées dans PostgreSQL. Le fichier .env.local choisit la base avec DATABASE_TARGET : local (Postgres dans Docker) ou remote (Supabase, puis Azure).
cp .env.example .env.local # puis renseigner BETTER_AUTH_SECRET (et DATABASE_URL_REMOTE si besoin)
npm install
npm run db:up # Postgres et Azurite (émulateur Azure Storage) dans Docker
npm run db:migrate # crée les tables sur la base choisie par DATABASE_TARGET
npm run dev
npm run test:e2e # la suite de tests : base de test kora_test recréée, e-mails écrits dans un fichier
DATABASE_URL_REMOTE = chaîne « Session pooler » (Connect › Session pooler), puis DATABASE_TARGET=remote npm run db:migrate. Toutes les tables ont la sécurité par ligne activée sans règle : l'API REST publique de Supabase n'y voit rien, l'application (propriétaire des tables) n'est pas concernée. Désactivez aussi l'API de données (Project Settings › Data API). Les variables NEXT_PUBLIC_SUPABASE_* ne servent pas.npm run db:generate : crée une migration après une modification de src/db/schema.ts.npm run db:studio : explore les données.audit_events) est en ajout seul : la base refuse toute modification ou suppression d'un événement.Trois bases, trois règles :
| Base | Qui la migre | Commande |
|---|---|---|
Test kora_test (Docker) | La suite de tests, à chaque lancement : recréée puis migrée par tests/e2e/global-setup.ts | Rien à faire : npm run test:e2e |
Développement kora (Docker) | Vous, après chaque migration ajoutée ou récupérée | npm run db:up puis npm run db:migrate, avec DATABASE_TARGET=local dans .env.local |
| En ligne (Supabase) | Seulement avec l'accord explicite du porteur du produit, avant de déployer le code qui en dépend | DATABASE_TARGET=remote npm run db:migrate |
Avant npm run db:migrate, vérifiez la valeur de DATABASE_TARGET dans .env.local : remote vise la base en ligne. La base locale kora est une copie de la base en ligne, avec de vraies adresses de clients : gardez MAIL_TRANSPORT=console.
Écrire une migration
src/db/schema.ts.npm run db:generate -- --name <sujet> : crée drizzle/<n>_<sujet>.sql et son instantané drizzle/meta/<n>_snapshot.json.--> statement-breakpoint puis ALTER TABLE "<table>" ENABLE ROW LEVEL SECURITY; (voir drizzle/0007_request_limits.sql).npm run db:migrate sur la base locale, puis npm run test:e2e.docker exec kora-postgres psql -U kora -d kora -c '\d <table>'.SQL écrit à la main (renommer une colonne, changer une clé, migrer des données) : npx drizzle-kit generate --custom --name <sujet>, puis écrire le SQL dans le fichier créé. Piège : pour un renommage, drizzle-kit generate pose une question interactive, impossible sans terminal (« Interactive prompts require a TTY »). Dans ce cas, mettez à jour à la main l'instantané drizzle/meta/<n>_snapshot.json du fichier personnalisé pour qu'il décrive le schéma après la migration, puis vérifiez que npx drizzle-kit generate répond « No schema changes, nothing to migrate ». Exemple : drizzle/0010_figma_personal_connections.sql.
Ne jamais modifier une migration déjà appliquée, même seulement sur votre base locale : la base retient qu'elle est passée et ne la rejouera pas. Écrivez-en une nouvelle.
L'application tourne sur Vercel, dans la région de Francfort (fra1, fixée par vercel.json), à côté de la base Supabase : une base éloignée multiplie la latence de chaque action. À chaque déploiement :
drizzle/, appliquez-les sur la base en ligne avant de déployer le code : DATABASE_TARGET=remote npm run db:migrate.DATABASE_TARGET=remote, DATABASE_URL_REMOTE (pooler Supabase de Francfort), DATABASE_SSL_CA (le texte PEM du certificat), BETTER_AUTH_SECRET, BETTER_AUTH_URL (l'adresse publique), TRUSTED_PROXY_HOPS=1, STORAGE_TARGET=remote et les STORAGE_*_REMOTE, RESEND_API_KEY, MAIL_FROM ; selon les besoins, GOOGLE_*, MICROSOFT_*, DEMO_REQUESTS_TO, MAX_VIDEO_UPLOAD_MB et FILE_LINK_SECONDS. Jamais MAIL_TRANSPORT en ligne. Vercel sépare les variables Production et Preview : cochez aussi Preview pour qu'une branche se construise, en sachant que sa préversion utilise alors la vraie base et le vrai Azure.Les fichiers vont dans Azure Blob Storage (src/lib/storage.ts), choisi par STORAGE_TARGET comme la base : local (Azurite, dans Docker) ou remote (compte Azure). Chaque agence a son propre conteneur privé, enregistré dans agency_storage avec son compte, pour pouvoir lui donner plus tard un compte à part. Dans le conteneur, les fichiers suivent l'organisation de l'agence :
agence-<agence>/projets/<id>/livrables/<id>/v<version>/<sha256>.<ext>
agence-<agence>/projets/<id>/livrables/<id>/couverture/<sha256>.<ext>
Un fichier qui pourrait exécuter du code s'il était ouvert à son adresse (SVG, page HTML…) est servi en téléchargement ; images, PDF, vidéos et sons s'ouvrent dans la page. Le canevas affiche toujours un SVG, en image (corrigé le 23 octobre 2026).
Le fichier est nommé par son empreinte SHA-256 : une version ne peut pas changer de contenu sans changer de nom. Les chemins utilisent les identifiants, pas les noms, et ne bougent pas si l'on renomme ou déplace un projet (la base fait foi). Les pages n'accèdent jamais au stockage : /api/files/<id> vérifie que la personne est membre de l'agence, ou client vérifié invité sur une revue qui montre ce fichier, puis redirige vers une adresse en lecture seule valable 5 minutes (FILE_LINK_SECONDS). Une vidéo se lit par morceaux, chacun avec cette adresse : la sienne dure donc sa longueur plus 15 minutes (longueur lue par le navigateur de l'agence à l'import, comptée jusqu'à 3 heures), et si elle expire quand même (pause, onglet laissé ouvert), le canevas en redemande une et reprend au même moment. La taille maximale d'un import est MAX_UPLOAD_MB (50 par défaut) ; une vidéo importée comme version a la sienne, MAX_VIDEO_UPLOAD_MB (1 024 par défaut), car un film pèse bien plus qu'une maquette.
L'import ne passe pas par l'application (dont l'hébergeur limite la taille des requêtes, 4,5 Mo sur Vercel) : la page calcule l'empreinte du fichier et demande une autorisation (POST /api/files/upload), qui vérifie session, place, taille et limite de débit. Elle reçoit une adresse d'écriture valable 15 minutes (une minute de plus par 25 Mo, 6 heures au plus), qui ne permet que de créer ce fichier-là (ni le lire, ni en remplacer un autre), et un jeton signé par le serveur. Elle envoie le fichier à Azure, puis demande sa validation (POST /api/files/complete) : le serveur relit le fichier dans le stockage et recalcule lui-même l'empreinte ; s'il ne correspond pas à celui annoncé, il est supprimé et refusé (src/lib/upload.ts, prepareUpload et completeUpload dans src/lib/storage.ts). Un fichier de toute taille, jusqu'à un film de plusieurs Go : la page lit le fichier par morceaux de 8 Mo pour en calculer l'empreinte (@noble/hashes, déjà utilisé par Better Auth), sans jamais le charger en entier, et au-delà de 8 Mo l'envoie en blocs, 4 à la fois, un bloc raté renvoyé seul, que le stockage assemble ensuite ; l'import affiche son avancement. Un envoi en blocs demande le droit d'écriture, et pas seulement de création : avant de relire le fichier, le serveur le verrouille donc par un bail Azure permanent, dont personne ne garde l'identifiant. Une fois enregistré, aucun fichier ne peut plus être remplacé, même avec l'adresse qui a servi à l'envoyer.
next.config.ts) : la version moderne exige des navigateurs très récents.files de Postgres) ne sont plus servis : réimportez-les si besoin.Un visiteur non connecté voit à / la page d'accueil publique (/decouvrir), avec « Réserver une démo » (/demo, transmise par e-mail à DEMO_REQUESTS_TO) et « Commencer » (/commencer, la création du compte). L'espace agence demande un compte (/connexion) : e-mail et mot de passe (12 caractères au moins, variés, rien de courant ni de personnel, src/lib/passwordPolicy.ts), ou Google et Microsoft si leurs clés sont renseignées. L'adresse est confirmée par un lien envoyé par e-mail avant la première connexion (les comptes créés avant cette règle sont considérés comme confirmés, migration 0008) ; une adresse Microsoft n'en est dispensée que si Microsoft la garantit, et une invitation d'agence ne s'accepte qu'avec une adresse confirmée.
Chaque compte travaille dans une agence. Après l'inscription, la personne crée la sienne (/bienvenue) et en devient propriétaire, puis met en place son premier espace, son premier projet et ses collègues (/demarrer), ou rejoint l'agence qui l'a invitée. Le propriétaire et les administrateurs invitent leurs collègues depuis le menu du compte, « Équipe » : l'invité reçoit un lien /invitation/:id qu'il ne peut accepter qu'avec l'adresse invitée. Le lien ne montre que le nom de l'agence et l'adresse masquée : l'invité saisit lui-même son adresse. Toutes les données sont rangées par agence (organization_id dans chaque clé primaire) et le serveur filtre chaque requête sur l'agence du membre ; retirer quelqu'un de l'équipe lui coupe l'accès immédiatement.
| Rôle | Peut faire |
|---|---|
| Membre | Tout le travail de production : voir, commenter, importer des versions, qualifier une demande, préparer un devis en brouillon, partager une revue, restaurer depuis la corbeille |
| Administrateur | En plus : envoyer un devis, répondre à une contre-proposition, révoquer un lien client, supprimer un espace ou un projet, vider la corbeille, gérer l'équipe |
| Propriétaire | En plus : supprimer l'agence, nommer un autre propriétaire |
Le serveur refuse ces actions à un membre (src/db/agencyPermissions.ts), l'interface les affiche grisées avec la raison.
Le client n'a ni compte ni mot de passe. L'agence l'invite par e-mail depuis « Partager la revue » ; il reçoit un lien personnel, qui ne contient qu'un identifiant aléatoire et montre son adresse masquée (c•••••@client.fr) : celui qui détient le lien n'apprend pas l'adresse, et le code est demandé par l'identifiant de l'invitation, puis envoyé à l'adresse invitée; sinon, rien ne lui révèle qu'elle existe. Tant que personne n'est invité, « Partager la revue » prévient que le lien copié ne s'ouvrira pour personne. Avec le lien copié-collé, seule une adresse invitée reçoit un code ; l'écran du code dit aux autres de demander une invitation à la personne qui leur a transmis le lien. Personne ne peut demander l'accès depuis Kora (retiré pour des raisons de sécurité le 21 octobre 2026) ; les demandes reçues avant se règlent encore dans « Partager la revue ». Retirer une invitation coupe l'accès immédiatement. Le lien ne donne accès qu'à la version partagée, et le serveur n'accepte du client que ses propres commentaires, ses décisions sur les propositions envoyées et la validation, signés de son adresse vérifiée (voir src/db/reviewAuth.ts et src/db/reviewAccess.ts).
Chaque invité a un rôle sur la revue (src/lib/clientRoles.ts). Un décideur valide la version, demande des modifications, accepte ou refuse les devis et fait des contre-propositions. Un contributeur consulte, commente et répond. L'agence choisit le rôle en invitant (décideur par défaut) et peut le changer dans « Partager la revue » ; une demande d'accès acceptée donne le rôle de contributeur. Le serveur refuse les décisions d'un contributeur, les relances ne partent qu'aux décideurs, et la page du client ne reçoit jamais la liste des autres invités.
« + Demande » sert aux demandes reçues ailleurs (e-mail, appel, réunion, Slack, Teams, Figma) ou portant sur tout le livrable. Sur la carte de la demande, une petite icône en bas à droite dit sa source : le logo officiel de Slack, Teams ou Figma (inchangé, comme leurs règles de marque le permettent pour indiquer une provenance), une icône de Kora sur une pastille colorée pour l'e-mail, l'appel, la réunion ou une autre source (src/components/SourceIcon.tsx). Une demande faite dans Kora, posée sur le livrable ou saisie sans source, n'en a pas : c'est le cas le plus courant, une marque sur chacune n'apportait rien (24 octobre 2026). Au survol et pour les lecteurs d'écran : « Demande reçue par e-mail le … ». Leur source (canal, date, extrait collé, capture) est visible par le client, qui voit ainsi qu'il s'agit de sa propre demande, et figure dans la preuve avec l'empreinte de la capture (src/lib/requestSource.ts). La capture est une image importée par l'agence (projets/<id>/livrables/<id>/demandes/), que le client n'ouvre que s'il voit la demande.
Le client voit ses propres demandes, et celles de l'agence dont elle a choisi le statut : « Inclus », « À clarifier » créée par « + Demande », et « Hors scope » une fois un devis envoyé. Un pin posé par l'agence sur le rendu, sans statut choisi, reste interne tant qu'elle ne l'a pas qualifié ; sa bulle l'indique. Sa qualification propose les mêmes trois choix que « + Demande » : « À clarifier » avec un titre le rend visible au client. Ce que l'agence a écrit pour elle-même reste interne : le texte du pin et les réponses écrites avant que le client voie la demande. Le client, comme la preuve, lit le titre et la description, puis les échanges qui suivent (src/lib/internalNotes.ts) ; l'éditeur marque ces messages « Interne ». Un devis en brouillon ne quitte jamais le serveur, pas même dans les données reçues par la page. Une demande cachée est introuvable pour le client, y compris par l'API (visibleToClient, src/db/reviewAccess.ts).
La page du client écrit une action à la fois, chacune sur sa route (src/db/reviewActions.ts). Le serveur construit la modification à partir de ce qu'il a en base, la passe par les règles du client, puis l'applique en une transaction, avec son événement d'audit et, pour une décision, la notification de l'agence. Une action que les règles refusent est refusée en entier (409).
Route (/api/review/:token/…) | Action |
|---|---|
POST comments | Commentaire sur la version, ou demande posée sur le site de recette (site) |
PATCH comments/:id | Modifier son commentaire (message) ou déplacer son repère (page, x, y) |
DELETE comments/:id | Supprimer son commentaire, tant que l'agence ne l'a pas qualifié |
POST comments/:id/replies | Répondre dans un fil de la version |
PATCH, DELETE comments/:id/replies/:replyId | Modifier ou supprimer sa réponse |
POST proposals/:id/decision | Accepter ou refuser un devis envoyé, une seule fois |
POST proposals/:id/adaptation | Demander une adaptation, si l'agence l'a permise |
POST changes-request | Demander des modifications |
GET versions | Les versions de ce livrable partagées avec ce client, chacune comme sa page la montrerait, pour les comparer |
POST approval | Valider la version, ce qui la verrouille ; refusé tant qu'un devis envoyé attend la décision du client (la fenêtre de validation les liste, à accepter ou refuser). Les remarques encore « À clarifier » sont abandonnées en validant : la fenêtre le dit et les liste, le journal les nomme |
Dans la cloche, chaque activité ouvre ce qu'elle concerne : une décision du client ouvre la demande, une demande d'accès reçue avant leur retrait ouvre « Partager la revue » sur le livrable, une relance ouvre la version (notificationHref, src/app/(agency)/routes.ts). Les notifications plus anciennes, ou dont le livrable a disparu, restent du simple texte.
Seul le client valide une version, depuis sa page : l'agence n'a aucune route pour le faire. L'agence suit le même chemin pour toutes ses écritures (src/db/agencyActions.ts, src/db/workspaceActions.ts) : le serveur signe chaque message avec le membre, refuse toute écriture sur une version validée ou remplacée et sur une demande dont le devis est tranché, et applique aux membres les réserves de src/db/agencyPermissions.ts. /api/state est en lecture seule : les pages lisent l'état et le relisent après chaque action.
Route (/api/comments…) | Action de l'agence |
|---|---|
POST /api/comments | Commentaire (deliverableId, message…), demande saisie par l'agence (request : « À clarifier », « Inclus » ou « Hors scope », avec sa source facultative : canal, date, extrait, capture), ou commentaire remis après sa suppression (restore) |
PATCH /api/comments/:id | Modifier un message de l'agence (message) ou déplacer un repère (page, x, y) |
DELETE /api/comments/:id | Supprimer un commentaire de l'agence |
PUT /api/comments/:id/closure | Clore une discussion (lecture seule pour tous, refusé tant qu'une proposition attend le client) ou la rouvrir |
PUT /api/comments/:id/qualification | « Inclus », « Hors scope » avec un chiffrage en brouillon, « À clarifier » avec un titre (visible du client), ou retour « À clarifier » sans titre |
POST /api/comments/:id/replies | Répondre |
PATCH, DELETE /api/comments/:id/replies/:replyId | Modifier ou supprimer une réponse de l'agence |
POST /api/comments/:id/proposal | Envoyer un premier devis écrit dans la fenêtre de proposition (administrateurs) |
POST /api/comments/:id/proposal/send | Envoyer le devis en brouillon (administrateurs) |
POST /api/comments/:id/proposal/versions | Nouvelle version : envoyée à la place du devis en attente (send: true, administrateurs), ou rechiffrage après un refus, en brouillon (send: false) |
POST /api/comments/:id/adaptation/decision | Accepter ou refuser la contre-proposition du client (administrateurs) |
POST /api/deliverables/:id/review-link | Lien client de la version : l'actif, ou un nouveau au jeton tiré par le serveur |
DELETE /api/deliverables/:id/review-link | Révoquer le lien (administrateurs) |
DELETE /api/deliverables/:id/review-link/invites/:email | Retirer une invitation, ce qui coupe l'accès de cette adresse |
src/db/reviewMails.ts) : un devis envoyé, ou la réponse à une contre-proposition, part par e-mail aux décideurs du client, avec leur lien personnel (« Répondre à » : l'administrateur qui l'a envoyé). Chaque décision du client (devis accepté ou refusé, contre-proposition, validation, demande de modifications) part au propriétaire et à tous les administrateurs de l'agence, avec un lien vers la demande (« Répondre à » : le client). Ni les membres ni les contributeurs ne les reçoivent. Un e-mail qui échoue est journalisé et n'annule jamais l'action.RESEND_API_KEY et MAIL_FROM (domaine vérifié dans Resend, avec SPF, DKIM et DMARC). Chaque e-mail part en HTML avec sa version texte, et avec un « Répondre à » vers le membre de l'agence concerné (ou MAIL_REPLY_TO) : l'adresse d'envoi n'a pas de boîte. MAIL_TRANSPORT=console n'envoie jamais rien et affiche les e-mails dans la console, même avec une clé Resend (tests, démonstrations). Sans clé, en développement, ils s'affichent dans la console ; ailleurs, l'agence copie le lien personnel de chaque invité depuis « Partager la revue ».Pour un livrable « Lien », dont l'adresse est le site de recette, le tableau du livrable (« Paramétrer la barre Kora ») donne une ligne de script à placer une seule fois sur le site, et le lien à envoyer au client (l'adresse du site suivie de ?kora, aussi dans les e-mails d'invitation et de relance) :
<script src="https://<kora>/kora-toolbar.js" data-site="<clé du livrable>" defer></script>
<Script> dans app/layout.tsx, WordPress, Webflow, Shopify, Tag Manager) et dit, en direct, quand la barre a été vue sur le site : son panneau demandé dans un cadre par le site du livrable lui-même (src/db/siteSeen.ts, un réglage de l'agence, écrit au plus toutes les 5 minutes, jamais envoyé au client).src/lib/siteKey.ts, GET /api/deliverables/:id/site-key) : /review/site/<clé> ouvre la dernière version partagée avec un lien actif, ou dit qu'aucune ne l'est. La ligne ne change donc ni avec les versions ni avec les liens, et le jeton du lien n'apparaît plus sur le site. Une ligne plus ancienne, avec data-review="<jeton>", ouvre toujours sa version.public/kora-toolbar.js) n'affiche la barre qu'à qui a ouvert le site avec ?kora, puis s'en souvient sur l'appareil. Il ouvre le panneau Kora (/review/:token/site) dans un cadre servi par Kora.frame-ancestors), et la page vérifie que c'est celle du livrable (reviewedSiteOrigin). Toutes les autres pages de Kora refusent toujours d'être affichées dans un cadre.kora_review_site_session, SameSite=None; Partitioned), que le navigateur ne garde que pour ce site.V, par défaut : le site fonctionne normalement) et « Commenter » (C : chaque clic sur un élément ouvre un commentaire, jusqu'au retour à « Naviguer » ou Échap), « Commentaires » (la liste de ceux de la page, à droite) et l'aide ; une aide s'affiche à la première ouverture. Un commentaire s'écrit et se lit dans une bulle à côté de l'épingle ou du clic, comme sur le canevas. Il passe par POST comments avec sa place (site : page, élément, position dans l'élément, largeur d'écran, src/lib/siteReview.ts), puis suit le parcours habituel côté agence.kora_agency_site_session, partitionnée, 12 heures, liée au membre, à l'agence et à la version ; l'appartenance et le lien sont relus à chaque requête : un membre retiré, un lien révoqué ou expiré la ferment aussitôt). L'agence y voit toutes les épingles, y compris ses brouillons, pose ses commentaires, répond et qualifie. Depuis le site, ses routes n'ouvrent que ces gestes () et seulement sur cette version : devis, contre-propositions et partage répondent , à faire dans Kora.Limites de l'essai : testé dans Chrome seulement (Safari et Firefox à vérifier à la main) ; le site de l'agence doit accepter le script et le cadre de Kora s'il a sa propre politique de sécurité ; les numéros des épingles diffèrent entre la vue de l'agence (pins internes compris) et celle du client.
Kora lira les commentaires des fichiers Figma de l'agence pour les qualifier. Un plugin Figma ne peut ni toucher à l'interface de Figma ni lire ses commentaires : seule l'API web de Figma le permet. Faits : la connexion (étape 1), le livrable « Figma » (étape 2a) et l'import des commentaires (étape 2b).
GET /api/integrations/figma/connect, puis /callback). Une connexion par membre et par agence (figma_connections, migration 0010) : les fichiers sont souvent partagés avec chaque designer, et Figma compte ses lectures par compte. Un membre ne voit et ne retire que la sienne. Un compte Figma connecté entre dans l'équipe de l'agence (figmaTeam) et y reste après une déconnexion : ses commentaires restent dans Figma. Figma ne garde qu'une clé active par compte : un même compte Figma connecté par un second membre retire la connexion du premier.state signé (10 minutes), un cookie de ce navigateur avec la même valeur et le vérificateur PKCE (src/lib/figmaOAuth.ts). Le code de Figma n'est enregistré que si les trois correspondent, et si le membre peut encore gérer l'agence.TOKEN_ENCRYPTION_KEY, src/lib/sealed.ts), dans figma_connections, une ligne par agence. Elles ne sont jamais envoyées à une page : GET /api/integrations/figma ne donne que le nom du compte Figma et qui l'a connecté.DELETE /api/integrations/figma). Figma ne permet pas de les révoquer : on retire aussi Kora dans les paramètres du compte Figma.<BETTER_AUTH_URL>/api/integrations/figma/callback et les autorisations de figmaScopes (src/lib/figma.ts). Puis FIGMA_CLIENT_ID, FIGMA_CLIENT_SECRET et TOKEN_ENCRYPTION_KEY (voir .env.example). Sans eux, la section Figma n'apparaît pas. Une application privée ne sert qu'à l'équipe Figma qui l'a créée : pour d'autres agences, Figma doit l'approuver.src/lib/figmaLink.ts). Kora lit le fichier avec le compte Figma du membre (GET /api/integrations/figma/files/:clé) : son nom, ses pages et les versions nommées de son historique (les enregistrements automatiques sans nom sont écartés). L'agence coche les pages en revue (les commentaires des autres resteront dans Figma) et choisit la version Figma envoyée au client.Sur le canevas, pdf.js dessine une page une fois, à sa taille ; le zoom l'étire, donc la floute. Une fois le zoom stabilisé (0,3 s), une copie plus fine de chaque page visible se dessine par-dessus (src/components/SharpPdfPage.tsx), par paliers (×1,5, ×2, ×3, ×4, fois la densité de l'écran). Rien ne clignote : la copie reste transparente tant qu'elle se dessine (la page de react-pdf est blanche sinon), et la copie du palier précédent reste affichée jusqu'à ce que la suivante soit prête. Pour ne pas remplir la mémoire : jamais de copie pour une page hors de l'écran (elle la perd en sortant), un budget de 10 millions de pixels par page, soit 40 Mo au plus (src/lib/pdfSharpness.ts), et la copie disparaît, mémoire comprise, quand on dézoome. Pas encore dans la comparaison de versions.
« Synthèse & export » produit, depuis la base et non depuis la page, la preuve d'une version : empreinte SHA-256 du fichier validé, chaque demande avec sa qualification et ses devis, chaque décision avec l'identité constatée par le serveur (membre de l'agence ou adresse e-mail vérifiée du client), l'heure du serveur et l'adresse IP, puis le journal complet de la version et l'empreinte du récapitulatif (src/db/proof.ts). Le journal est en ajout seul, IP et navigateur compris.
x-forwarded-for : en ligne, renseignez TRUSTED_PROXY_HOPS (1 derrière Azure Front Door ou Vercel) pour ne garder que l'adresse ajoutée par le proxy ; sans proxy déclaré, l'adresse est marquée « non vérifiée », car un visiteur peut envoyer son propre en-tête.POST /api/deliverables/:id/reopen | Rouvrir la validation, avec la raison dans le journal |
PATCH /api/deliverables/:id/review-link | Expiration du lien après 7 jours, ou jamais (autoExpire) |
POST /api/spaces, PATCH, DELETE /api/spaces/:id | Créer, renommer, supprimer un espace (suppression : administrateurs) |
POST /api/projects, PATCH, DELETE /api/projects/:id | Créer, renommer (name), déplacer (clientId), supprimer un projet (suppression : administrateurs) |
POST /api/deliverables | Nouveau livrable, ses fichiers importés au préalable |
POST /api/deliverables/:id/versions | Nouvelle version : les précédentes sont figées |
PATCH, DELETE /api/deliverables/:id | Renommer (name), déplacer (projectId) ou mettre à la corbeille toutes les versions d'un livrable |
POST /api/trash/:id/restore | Restaurer un élément de la corbeille |
DELETE /api/trash/:id, DELETE /api/trash, DELETE /api/trash/expired | Supprimer définitivement un élément, tout, ou ce qui a plus de 30 jours (administrateurs) |
siteAgencyActions403 KORA_ONLYmodern-screenshot, chargé depuis Kora au premier clic). Le panneau l'envoie (POST /api/review/:token/site/capture : image JPEG ou PNG, 3 Mo au plus), puis la demande la nomme. La barre dessine dans l'image le cadre de l'élément visé et le point du clic : la capture se suffit à elle-même, où qu'on l'ouvre. Elle se voit dans le fil de la demande, côté agence comme côté client, et la preuve garde l'empreinte de cette image annotée. C'est une copie reconstituée, pas une photo de l'écran : une image d'un autre site qui refuse d'être lue y reste blanche. Si la capture échoue (site qui bloque les scripts de Kora, Safari capricieux), la demande part sans.ThreadPopover : réponses, modification, qualification par l'agence). Sur le site, les épingles sont celles de Kora : initiales de l'auteur sur un commentaire, icône et couleur du statut sur une demande.https : Kora refuse, à la création comme pour une nouvelle version, une adresse de cet ordinateur, du réseau local ou en http (une page en http peut être modifiée en chemin, et le client n'ouvrirait pas une adresse locale). Une nouvelle version demande l'adresse du site, pré-remplie avec celle de la précédente.src/app/(agency)/editor/SiteBoard.tsx), côté agence comme côté client : une feuille en forme de fenêtre de navigateur qui dit quoi faire (le paramétrage pour l'agence, les étapes pour le client), puis chaque demande comme une page du canevas, sa capture avec son épingle là où le clic a eu lieu. L'agence peut épingler ses propres brouillons sur une capture et les déplacer. « Ouvrir le site » ouvre la recette avec la barre.src/lib/siteHandoff.ts), échangé contre la session habituelle tant que l'adresse reste invitée.src/lib/figmaReceipt.ts429 FIGMA_RATE_LIMITEDfigma dans le livrable (fileKey, fileName, pages, reviewed, version) vient de Figma.src/app/(agency)/editor/FigmaBoard.tsx) dit quel fichier, quelle version et quelles pages sont en revue, avec « Ouvrir dans Figma ». Le client y lit comment commenter dans Figma.FIGMA_COMMENTS=1 remet le choix « Dans Figma » dans « Nouveau livrable » (figmaCommentsOffered, src/lib/figmaLink.ts). Un livrable créé ainsi avant fonctionne toujours. Ce qui suit sur le livrable « Figma » et l'import des commentaires décrit ce mode.src/db/figmaScreens.ts) : c'est ce que propose « Figma » dans « Nouveau livrable ». Kora lit les écrans du fichier (cadres en haut de chaque page, et ceux rangés dans une section) ; elle coche ceux à faire valider (30 au plus), chacun dans l'un des quatre formats de Figma, dans son ordre : PNG (par défaut), JPEG, SVG ou PDF. Un menu « Format de tous les écrans » les règle d'un coup, un menu par écran fait les exceptions ; PNG et JPEG sont dessinés en 2x. Figma dessine un format par demande (POST …/screens/draw) : un import qui mélange les formats coûte autant de lectures de catégorie 1, et la fenêtre le dit. Puis le serveur recopie chaque écran dans le stockage de l'agence (POST …/screens/store, une requête par écran, avec la progression affichée) : uniquement depuis le stockage de Figma, reconnu à ses octets comme le format demandé. Un SVG ouvert à son adresse est téléchargé, jamais exécuté ; le canevas l'affiche en image. Le livrable (Screens) est fait de ces copies, avec les noms et tailles de la lecture du serveur (attestation signée), jamais ceux de la page.
src/app/(agency)/ScreensLayoutDialog.tsx) : les vignettes des copies avec leur nom, dans l'ordre de Figma, que l'agence réordonne en les glissant ou avec leurs flèches (au clavier), et le nombre d'écrans par ligne (1 à 6). « Importer » crée le livrable dans cet ordre. Une grille, pas un placement libre : des écrans éloignés dans Figma garderaient un écart inutile./api/files).figmaOf, src/db/proof.ts).src/db/figmaImport.ts, POST /api/deliverables/:id/figma-import) : avec le compte Figma de qui a créé la tournée (figmaImports, réglage de l'agence) ; si ce compte n'est plus connecté, le membre qui importe reprend la tournée avec le sien. Kora va chercher les commentaires du fichier à l'ouverture du livrable et sur « Actualiser » (au plus toutes les 15 secondes, FIGMA_IMPORT_QUIET_SECONDS), dans la tournée en cours seulement. Les notifications de Figma (webhooks) viendront à l'étape 3.
figmaTeam de l'agence), les fils résolus avant l'import et ceux des pages décochées. Cocher une personne retire de Kora ses fils encore non qualifiés.visibleToClient). Qualifié, il le voit en entier. Le client ne peut ni modifier ni supprimer des mots venus de Figma (clientWords, src/db/reviewAccess.ts).comment.imported au journal.figmaAccessToken, src/db/figmaConnection.ts). Si Figma la refuse, l'agence reconnecte Figma.tests/e2e/figmaStandIn.ts) tient sa place.