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.
Funktionsweise
Abschnitt betitelt „Funktionsweise“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).
Schritte
Abschnitt betitelt „Schritte“-
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 überapp_db:im Manifest bindet.app_db:akzeptiert den Ressourcen-Namen (die ergonomische Form) oder seine UUID —services::apps::resolve_app_db_reflöst ihn auf. Beim Speichern wird das pro-App-Schemaapp_<id>der App sichergestellt und der RLS-Provisioner ausgeführt (ADR 0023 v4).Statt einer eigenen Ressource akzeptiert
app_db:auch das reservierte Tokenbuiltin— die serververwaltete interne App-Datenbank. Die erste App, die es nutzt, legt einmalig einebuiltin_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. -
Deklariere die Funktion in
backend_functions:.Füge einen Eintrag pro Funktion hinzu.
operationistquery(NUR-LESENDSELECT/WITH) oderexecute(INSERT/UPDATE/DELETE/MERGE, kein DDL).sqlist das autor-feste, parametrisierte Statement mit$1..$n-Platzhaltern.paramssind beschreibende Metadaten für die aufrufende UI (ein Label plus ein optionalertext | number | boolean-Hinweis); falls vorhanden, ist ihre Länge die erwartete Anzahl positionaler Argumente.app_db: workspace_app_dbbackend_functions:- name: overviewoperation: querysql: "SELECT id, total FROM ledger WHERE region = $1"params:- name: regionkind: text- name: mark_paidoperation: executesql: "UPDATE ledger SET paid = true WHERE id = $1"returning: falseSetze für eine
execute-Funktionreturning: true, um dieRETURNING-Zeilen des DML an den Aufrufer zurückzugeben; für einequerywird es ignoriert. -
Speichere die App.
Beim Speichern erzwingt
validate::validate_backend_functions(server/crates/sc-svc-projects/src/apps/validate.rs) der Reihe nach: jedernameist nicht leer und innerhalb der App eindeutig; jedessqlbesteht die passende Allowlist für seineoperation; und — falls irgendeine Funktion deklariert ist — muss die App eineapp_db:-Ressource binden. Eine fehlerhafte oder gefährliche Funktion kann daher niemals persistiert werden. -
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
BackendFnInfozurück —name,operationundparams— wobei das SQL zurückgehalten wird. -
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.operationsagt dem Aufrufer, wie er sie liest: einequerygibtrowsmitrow_count = rows.len()zurück; einexecutegibt die Anzahl der betroffenen Zeilen inrow_countund etwaigeRETURNING-Zeilen inrowszurück.{ "function": "overview", "operation": "query","rows": [{ "id": 1, "total": 940 }], "row_count": 1 } -
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 überPOST /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 Appoverview()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, derappsApi.listBackendFnsundappsApi.invokeBackendFngegen dieselben zwei Endpunkte aufruft.
Das Guard- und Sicherheitsmodell
Abschnitt betitelt „Das Guard- und Sicherheitsmodell“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): einequerymuss zu einem einzelnenSELECT/WITHparsen und darf keine Write-/DDL-Tokens enthalten; einexecutemuss zu einem einzelnenINSERT/UPDATE/DELETE/MERGEohne 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..$ngebunden; 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
queryläuft in einerBEGIN READ ONLY-Transaktion mitdefault_transaction_read_only = on(db_guarded_exec::exec_guarded_query), sodass selbst eine fehldeklariertequery, deren SQL tatsächlich DML ist, von Postgres auf Transaktionsebene abgelehnt wird. - RLS-
supacloud.app_id-Backstop. Bevor das Statement läuft, injiziert der KernelSET LOCAL supacloud.app_idaus 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_dbist der stets aktive Backstop, unabhängig vom Zugriffspfad. - Begrenzter Schadensradius. Ein Statement-Timeout pro Aufruf (
10000ms) und ein hartes Zeilenlimit (1000Zeilen) halten eine synchrone Lesung unter einer Sekunde und verhindern, dass eine unbegrenzte Nutzlast über den Callback zurückgestreamt wird.
Siehe auch
Abschnitt betitelt „Siehe auch“server/crates/sc-svc-projects/src/apps/manifest.rs—BackendFunctionDecl-Formserver/crates/sc-svc-projects/src/apps/validate.rs—validate_backend_functionsserver/crates/sc-svc-projects/src/app_deployments/backend_fn.rs— der Executorserver/crates/sc-svc-engine/src/workflows/db_guarded_exec.rs— der gemeinsame bewachte Kernelweb/src/lib/components/apps/AppBackendTab.svelte— der App-Tab Execute- ADR 0053 — bewachte synchrone Backend-Funktionen
- Eine Datenbank-Migration hinzufügen