Superfasttt

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 :

PhaseQuiQuandAvec quel jeton
SetupUn administrateur de l'espace clientUne seule foisSon jeton utilisateur admin (sa session Superfasttt)
UsageChaque utilisateur finalÀ chaque ouvertureAucun — 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.json

1.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();
}
// 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

ErreurCauseAction
403 origin_requiredÉchange serveur sans header OriginAjouter Origin (∈ allowed_origins) à l'appel
403 origin_not_allowedOrigin absente de allowed_originsCorriger le manifest, réimporter, réinstaller
401 token_invalid_or_used_or_expiredlaunch_token déjà utilisé / expiréRelancer l'app pour un nouveau token
422 à l'écrituredata non conforme au schéma de la collectionRespecter required / enum / additionalProperties du manifest

Voir aussi

On this page