Zum Inhalt springen
Farbschema wählenSprache wählen

App-Backend-Funktionen erstellen

Eine bewachte synchrone App-Backend-Funktion ist das App-Runtime-Äquivalent zu einem Windmill-Script: eine benannte, autor-feste SQL-Operation über die gebundene Datenbank der App, aufgerufen über den deployment-spezifischen Callback der App als typisierte Anfrage/Antwort. Sie läuft synchron unter demselben Datenbank-Guard, den die Workflow-Nodes db_query / db_execute verwenden — es gibt keine runs-Zeile und kein Polling. Reserviere Workflows für echte Orchestrierung (mehrstufig, externe Aufrufe, dauerhaft, Retries, Human-Gates); greife für eine Lesung oder einen einfachen Write zu einer Backend-Funktion.

Die Funktion wird im manifest_yaml der App deklariert und in BackendFunctionDecl geparst (server/crates/sc-svc-projects/src/apps/manifest.rs). Zwei Endpunkte stellen sie bereit (server/crates/sc-iface-http/src/interfaces/http/apps_backend_routes.rs):

Endpunkt Zweck
GET /api/apps/{id}/backend Der Funktions-Katalog — Name, operation und Argument-Metadaten. Niemals das SQL.
POST /api/apps/{id}/backend/{function_name} Aufruf — Body { "args": [...] }, positional, gebunden an $1..$n.

Beide werden entweder durch ein deployment-spezifisches App-Token (Bearer scwa_…) oder eine angemeldete Session authentifiziert — aufgelöst von resolve_submit_auth (apps_runs_auth.rs), demselben Pfad, den der Run-Submit-Endpunkt verwendet. Der serverseitige Code einer deployten App ruft sie mit ihrem scwa_-Token auf; der Operator durchsucht sie über den Tab Execute der App-Detailansicht (web/src/lib/components/apps/AppBackendTab.svelte).

  1. Binde eine app_db:-Ressource.

    Eine Backend-Funktion braucht eine Datenbank, gegen die sie läuft, daher erfordert die Deklaration irgendeiner Funktion, dass die App eine postgresql-Ressource über app_db: im Manifest bindet. app_db: akzeptiert den Ressourcen-Namen (die ergonomische Form) oder seine UUID — services::apps::resolve_app_db_ref löst ihn auf. Beim Speichern wird das pro-App-Schema app_<id> der App sichergestellt und der RLS-Provisioner ausgeführt (ADR 0023 v4).

    Statt einer eigenen Ressource akzeptiert app_db: auch das reservierte Token builtin — die serververwaltete interne App-Datenbank. Die erste App, die es nutzt, legt einmalig eine builtin_app_db-Ressource für den Workspace an, die von allen Apps geteilt wird (jede App ist in ihrem eigenen Schema mit least-privilege Rolle unter RLS isoliert). Sie wird aus Plattform-Zugangsdaten abgeleitet und gehört dem Server, erscheint unter Ressourcen daher schreibgeschützt (ein System-Badge; Test, Bearbeiten, Löschen und RLS-Reprovisionierung sind deaktiviert) — du konfigurierst oder testest sie nie selbst.

  2. Deklariere die Funktion in backend_functions:.

    Füge einen Eintrag pro Funktion hinzu. operation ist query (NUR-LESEND SELECT/WITH) oder execute (INSERT/UPDATE/DELETE/MERGE, kein DDL). sql ist das autor-feste, parametrisierte Statement mit $1..$n-Platzhaltern. params sind beschreibende Metadaten für die aufrufende UI (ein Label plus ein optionaler text | number | boolean-Hinweis); falls vorhanden, ist ihre Länge die erwartete Anzahl positionaler Argumente.

    app_db: workspace_app_db
    backend_functions:
    - name: overview
    operation: query
    sql: "SELECT id, total FROM ledger WHERE region = $1"
    params:
    - name: region
    kind: text
    - name: mark_paid
    operation: execute
    sql: "UPDATE ledger SET paid = true WHERE id = $1"
    returning: false

    Setze für eine execute-Funktion returning: true, um die RETURNING-Zeilen des DML an den Aufrufer zurückzugeben; für eine query wird es ignoriert.

  3. Speichere die App.

    Beim Speichern erzwingt validate::validate_backend_functions (server/crates/sc-svc-projects/src/apps/validate.rs) der Reihe nach: jeder name ist nicht leer und innerhalb der App eindeutig; jedes sql besteht die passende Allowlist für seine operation; und — falls irgendeine Funktion deklariert ist — muss die App eine app_db:-Ressource binden. Eine fehlerhafte oder gefährliche Funktion kann daher niemals persistiert werden.

  4. Liste den Katalog auf.

    Terminal window
    curl -s https://app.example.com/api/apps/$APP_ID/backend \
    -H "Authorization: Bearer $SCWA_TOKEN"

    Gibt die deklarierten Funktionen als BackendFnInfo zurück — name, operation und params — wobei das SQL zurückgehalten wird.

  5. Rufe eine Funktion auf.

    Übergib die positionalen Argumente in args, in Deklarationsreihenfolge:

    Terminal window
    curl -s -X POST https://app.example.com/api/apps/$APP_ID/backend/overview \
    -H "Authorization: Bearer $SCWA_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "args": ["EU"] }'

    Die Antwort ist ein BackendFnResponse. operation sagt dem Aufrufer, wie er sie liest: eine query gibt rows mit row_count = rows.len() zurück; ein execute gibt die Anzahl der betroffenen Zeilen in row_count und etwaige RETURNING-Zeilen in rows zurück.

    { "function": "overview", "operation": "query",
    "rows": [{ "id": 1, "total": 940 }], "row_count": 1 }
  6. Rufe sie aus dem App-Frontend auf.

    Eine deployte App ruft dieselben Endpunkte mit ihrem deployment-spezifischen scwa_-Token auf (präge/rotiere es nur als Owner über POST /api/apps/{id}/deployment/token). Das Token wird serverseitig vom Backend der App verwendet — niemals im öffentlichen Frontend-Bundle eingebettet — sodass der eigene Server der App overview() aufruft und die Zeilen an seine UI zurückgibt. Auf der Operator-Seite kannst du jede Funktion ad-hoc über den Tab Execute der App-Detailansicht ausführen, der appsApi.listBackendFns und appsApi.invokeBackendFn gegen dieselben zwei Endpunkte aufruft.

Der Guard ist von seiner Verpackung entkoppelt — ein Entscheidungspunkt, identisch durchgesetzt, egal ob das SQL als Workflow-Node verpackt oder als synchrone Funktion aufgerufen wird. Er ist geschichtet:

  • Statement-Allowlist (services::workflows::security::sql_guard, sqlparser): eine query muss zu einem einzelnen SELECT/WITH parsen und darf keine Write-/DDL-Tokens enthalten; ein execute muss zu einem einzelnen INSERT/UPDATE/DELETE/MERGE ohne DDL-Tokens parsen. Durchgesetzt beim Speichern und beim Aufruf erneut bestätigt (Defense-in-Depth gegen Manifest-Drift).
  • Nur positionale Bindung. Argumente werden an $1..$n gebunden; das SQL selbst ist im Manifest fest. Es gibt keine String-Interpolation, also trägt die Leitung kein injizierbares SQL.
  • NUR-LESEND-Laufzeit-Backstop. Eine query läuft in einer BEGIN READ ONLY-Transaktion mit default_transaction_read_only = on (db_guarded_exec::exec_guarded_query), sodass selbst eine fehldeklarierte query, deren SQL tatsächlich DML ist, von Postgres auf Transaktionsebene abgelehnt wird.
  • RLS-supacloud.app_id-Backstop. Bevor das Statement läuft, injiziert der Kernel SET LOCAL supacloud.app_id aus der App-Identität, niemals aus der Anfrage, sodass die RLS-Policies der App Zeilen nach App filtern und ein Aufrufer nicht fälschen kann, welche App-Daten er sieht. RLS / app_db ist der stets aktive Backstop, unabhängig vom Zugriffspfad.
  • Begrenzter Schadensradius. Ein Statement-Timeout pro Aufruf (10000ms) und ein hartes Zeilenlimit (1000 Zeilen) halten eine synchrone Lesung unter einer Sekunde und verhindern, dass eine unbegrenzte Nutzlast über den Callback zurückgestreamt wird.
  • server/crates/sc-svc-projects/src/apps/manifest.rsBackendFunctionDecl-Form
  • server/crates/sc-svc-projects/src/apps/validate.rsvalidate_backend_functions
  • server/crates/sc-svc-projects/src/app_deployments/backend_fn.rs — der Executor
  • server/crates/sc-svc-engine/src/workflows/db_guarded_exec.rs — der gemeinsame bewachte Kernel
  • web/src/lib/components/apps/AppBackendTab.svelte — der App-Tab Execute
  • ADR 0053 — bewachte synchrone Backend-Funktionen
  • Eine Datenbank-Migration hinzufügen