Exemple complet (de bout en bout)
Un scénario complet et copier-coller — qui fait quoi, à quel moment — de l'enregistrement de l'app jusqu'à l'écriture d'une donnée depuis un frontend Next.js.
Cette page rassemble tous les steps d'une intégration réelle, dans l'ordre,
avec un frontend Next.js qui échange le launch_token côté serveur (le
schéma recommandé : l'access_token ne touche jamais le navigateur).
Le fil rouge est une app "Audit Hello" qui stocke des inspections.
Qui fait quoi (important)
Deux phases distinctes, à ne pas confondre :
| Phase | Qui | Quand | Avec quel jeton |
|---|---|---|---|
| Setup | Un administrateur de l'espace client | Une seule fois | Son jeton utilisateur admin (sa session Superfasttt) |
| Usage | Chaque utilisateur final | À chaque ouverture | Aucun — la plateforme émet un launch_token jetable |
Le jeton admin ne sert qu'au setup (valider / importer / installer), et il
reste côté plateforme. Votre app externe ne reçoit jamais ce jeton admin :
à l'usage, elle ne voit que le launch_token jetable passé dans l'URL, qu'elle
échange contre un access_token court. Inutile donc qu'un développeur soit
présent à chaque lancement.
Phase 1 — Setup (admin, une seule fois)
Toutes les commandes utilisent le jeton d'un utilisateur admin de l'espace
client et l'en-tête X-Tenant-ID.
1.1 Valider puis importer le manifest
manifest-request.json (enveloppe { "manifest": ... }) :
{
"manifest": {
"manifest_version": "2.0",
"id": "audit-hello",
"name": "Audit Hello",
"version": "1.0.0",
"description": "Démo inspections.",
"runtime": {
"type": "external",
"entry_url": "https://audit-hello.example.com",
"allowed_origins": ["https://audit-hello.example.com"]
},
"permissions": ["app_data:read", "app_data:write"],
"collections": {
"inspections": {
"schema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"status": { "type": "string", "enum": ["draft", "done"] }
},
"required": ["title", "status"],
"additionalProperties": false
}
}
}
}
}TENANT="<espace_client>"
TOKEN="<jeton_admin>"
API="https://api.superfasttt.ai"
# valider (ne modifie rien)
curl -X POST $API/api/v1/app-platform/manifests/validate \
-H "Authorization: Bearer $TOKEN" -H "X-Tenant-ID: $TENANT" \
-H "Content-Type: application/json" -d @manifest-request.json
# importer (enregistre la version)
curl -X POST $API/api/v1/app-platform/manifests/import \
-H "Authorization: Bearer $TOKEN" -H "X-Tenant-ID: $TENANT" \
-H "Content-Type: application/json" -d @manifest-request.json1.2 Installer l'app
curl -X POST $API/api/v1/app-platform/apps/audit-hello/install \
-H "Authorization: Bearer $TOKEN" -H "X-Tenant-ID: $TENANT"Réponse : state: active + granted_permissions. Le setup est terminé.
Phase 2 — Votre frontend (Next.js, échange côté serveur)
Trois fichiers suffisent. L'access_token est confiné dans un cookie httpOnly,
jamais lisible par le JavaScript du navigateur.
Next.js n'est qu'un exemple. N'importe quelle stack fonctionne — SPA pure
navigateur (React, Vue, vanilla JS), curl, ou n'importe quel backend (PHP,
Python, Go, Node…). La seule règle universelle : depuis un navigateur l'Origin
est posé automatiquement, depuis un appel serveur vous devez l'ajouter
vous-même. Pour une app entièrement navigateur, voir l'exemple fetch du
Quickstart.
2.1 Client API (le header Origin est obligatoire côté serveur)
// lib/sf.ts
const API = process.env.SF_API ?? "https://api.superfasttt.ai";
// Doit être une valeur de runtime.allowed_origins du manifest.
const ORIGIN = process.env.APP_ORIGIN ?? "https://audit-hello.example.com";
export async function exchange(launchToken: string) {
const r = await fetch(`${API}/api/v1/app-platform/runtime/exchange`, {
method: "POST",
// En appel serveur→serveur, le navigateur ne pose pas d'Origin :
// on le fournit nous-mêmes, sinon 403 origin_required.
headers: { "Content-Type": "application/json", Origin: ORIGIN },
body: JSON.stringify({ launch_token: launchToken }),
cache: "no-store",
});
if (!r.ok) throw new Error(`exchange ${r.status}: ${await r.text()}`);
return r.json() as Promise<{ access_token: string; scopes: string[] }>;
}
export async function listInspections(token: string) {
const r = await fetch(`${API}/api/v1/app-data/inspections?sort=-created_at`, {
headers: { Authorization: `Bearer ${token}`, Origin: ORIGIN },
cache: "no-store",
});
if (!r.ok) throw new Error(`list ${r.status}`);
return r.json();
}
export async function createInspection(
token: string,
data: { title: string; status: "draft" | "done" },
) {
const r = await fetch(`${API}/api/v1/app-data/inspections`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Origin: ORIGIN,
},
body: JSON.stringify({ data }),
cache: "no-store",
});
if (!r.ok) throw new Error(`create ${r.status}: ${await r.text()}`);
return r.json();
}2.2 Réception du lancement → échange → cookie
// app/api/launch/route.ts
import { NextRequest, NextResponse } from "next/server";
import { exchange } from "@/lib/sf";
export async function GET(req: NextRequest) {
const launchToken = req.nextUrl.searchParams.get("launch_token");
if (!launchToken) {
return NextResponse.redirect(new URL("/?error=missing_token", req.url));
}
try {
const { access_token } = await exchange(launchToken);
const res = NextResponse.redirect(new URL("/inspections", req.url));
res.cookies.set("sf_session", access_token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 15, // l'access_token vit ~15 min
});
return res;
} catch (e) {
const msg = e instanceof Error ? e.message : "exchange_failed";
return NextResponse.redirect(new URL(`/?error=${encodeURIComponent(msg)}`, req.url));
}
}entry_url pointe sur la racine de votre app ; renvoyez simplement vers ce
Route Handler (ou faites-le échanger directement à la racine).
2.3 Lire et écrire les inspections
// app/inspections/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createInspection, listInspections } from "@/lib/sf";
async function addInspection(formData: FormData) {
"use server";
const token = (await cookies()).get("sf_session")?.value;
if (!token) redirect("/?error=session_expired");
await createInspection(token!, {
title: String(formData.get("title")),
status: "draft",
});
redirect("/inspections");
}
export default async function Page() {
const token = (await cookies()).get("sf_session")?.value;
if (!token) redirect("/?error=session_expired");
const { items } = await listInspections(token!);
return (
<main>
<form action={addInspection}>
<input name="title" placeholder="Titre" required />
<button type="submit">Créer</button>
</form>
<ul>
{items.map((r: any) => (
<li key={r.id}>{r.data.title} — {r.data.status}</li>
))}
</ul>
</main>
);
}Phase 3 — Usage (chaque utilisateur, sans développeur)
L'utilisateur ouvre l'app depuis Superfasttt. La plateforme génère un
launch_token pour lui et le redirige vers votre entry_url :
curl -X POST $API/api/v1/app-platform/apps/audit-hello/launch-token \
-H "Authorization: Bearer <jeton_utilisateur>" -H "X-Tenant-ID: $TENANT"
# → { "launch_token": "...", "entry_url": "https://audit-hello.example.com?launch_token=...", "expires_at": "..." }L'utilisateur arrive sur entry_url, votre Route Handler échange le token,
pose le cookie et affiche les inspections. L'access_token obtenu porte le
claim sub = l'utilisateur qui a lancé — c'est ainsi que votre app sait
qui agit, même avec des milliers d'utilisateurs.
Pas de refresh token : quand l'access_token expire (~15 min), l'utilisateur
relance l'app depuis Superfasttt pour obtenir un nouveau launch_token.
Erreurs fréquentes
| Erreur | Cause | Action |
|---|---|---|
403 origin_required | Échange serveur sans header Origin | Ajouter Origin (∈ allowed_origins) à l'appel |
403 origin_not_allowed | Origin absente de allowed_origins | Corriger le manifest, réimporter, réinstaller |
401 token_invalid_or_used_or_expired | launch_token déjà utilisé / expiré | Relancer l'app pour un nouveau token |
422 à l'écriture | data non conforme au schéma de la collection | Respecter required / enum / additionalProperties du manifest |
Voir aussi
- Quickstart — version pas-à-pas.
- Authentification & sécurité — règles de session, origins, scopes.
- Référence du manifest.

