Benutzerdefinierte Funktionen
Übersicht
Abschnitt betitelt „Übersicht“Benutzerdefinierte Funktionen ermöglichen es Ihnen, beliebige Hono-API-Routen neben den automatisch generierten CRUD-Endpunkten von Rebase hinzuzufügen. Sie folgen demselben Muster der dateibasierten Erkennung wie Collections und Cronjobs: Legen Sie eine TypeScript-Datei in Ihrem functions/-Verzeichnis ab, und Rebase bindet sie automatisch ein.
Nutzen Sie benutzerdefinierte Funktionen für:
- Endpunkte für Geschäftslogik – Genehmigungen, Werbeaktionen, benutzerdefinierte Workflows
- Drittanbieter-Integrationen – Stripe-Webhooks, Slack-Befehle, externe API-Proxys
- Öffentliche Endpunkte – Kontaktformulare, Lead-Erfassung, Health-Checks
- Aggregierte Abfragen – Dashboard-Statistiken, Berichte, Analysen
Definieren einer benutzerdefinierten Funktion
Abschnitt betitelt „Definieren einer benutzerdefinierten Funktion“Erstellen Sie eine Datei in Ihrem Verzeichnis backend/functions/, die standardmäßig eine Hono-App exportiert:
// backend/functions/hello.tsimport { defineFunction } from "@rebasepro/server/functions";
export default defineFunction((app) => { app.post("/", async (c) => { const { name } = await c.req.json<{ name?: string }>().catch(() => ({ name: undefined })); return c.json({ message: `Hello, ${name ?? "world"}!` }); });});Dies wird unter /api/functions/hello eingebunden. Der Dateiname (ohne Erweiterung) wird zum Routenpräfix.
POST, da dies standardmäßig vom SDK gesendet wird – siehe Vom Client aufrufen. Eine GET-Route ist ebenso gültig; der Aufrufer muss dann { method: "GET" } angeben.
rebase dev überwacht das functions-Verzeichnis. Eine Datei, die hinzugefügt wird, während es läuft, wird beim nächsten Neuladen eingebunden – kein Neustart erforderlich. (Es muss mitgeteilt werden: Das Verzeichnis wird gescannt statt importiert, sodass der Watcher dies nicht ableiten kann.)
Vom Client aufrufen
Abschnitt betitelt „Vom Client aufrufen“import { createRebaseClient } from "@rebasepro/client";
const client = createRebaseClient({ baseUrl: "http://localhost:3000" });
const { message } = await client.functions.invoke<{ message: string }>( "hello", // the filename, without extension — one path segment { name: "Ada" } // JSON body; omitted for a GET);invoke erstellt die URL, hängt das Token des Aufrufers an und wirft bei einem Nicht-2xx-Status einen RebaseApiError – sodass die eigene Fehlerstruktur der Funktion den Aufrufer erreicht, anstatt einer reinen fetch-Zurückweisung.
Vier Dinge, die es neben dem Namen akzeptiert:
// A different method. The payload is dropped for GET, since GET has no body.await client.functions.invoke("hello", undefined, { method: "GET" });
// A sub-path — `/api/functions/hello/stats`. It goes here, never in the name:// a name containing "/" is refused rather than percent-encoded into a 404.await client.functions.invoke("hello", undefined, { method: "GET", path: "stats" });
// A query string. Passed as `path`, with no separator inserted before `?`.await client.functions.invoke("reports", undefined, { method: "GET", path: "?days=30" });
// A function that answers text or a file (`c.text(csv)`, `c.body(pdf)`).// The answer is read as JSON unless you say otherwise: "text" gives a string,// "blob" a Blob, "response" the Response itself with its body unread.const csv = await client.functions.invoke<string>("export-orders", undefined, { method: "GET", responseType: "text" });Importieren Sie aus @rebasepro/server/functions, nicht aus @rebasepro/server.
Beides funktioniert. Der Subpfad ist die portable Autorenoberfläche: Er bindet nichts ein, was Node erfordert, sodass eine dagegen geschriebene Funktion auf jeder JavaScript-Laufzeitumgebung ausgeführt werden kann. Das Paket-Root greift auf das gesamte Framework zu – die Boot-Sequenz, die Datei-Loader, die WebSocket-Schicht –, was für einen Server-Einstiegspunkt richtig ist, aber mehr darstellt, als ein Route-Handler benötigt. Zudem bietet er Ihnen typisierte Kontext-Zugriffsmethoden (getUser, getDriver), anstatt c.get("user") manuell casten zu müssen.
Siehe Laufzeit-Portabilität für den vollständigen Vertrag.
Konfiguration
Abschnitt betitelt „Konfiguration“Aktivieren Sie benutzerdefinierte Funktionen, indem Sie functionsDir zu Ihrer Backend-Konfiguration hinzufügen:
import path from "path";
const instance = await initializeRebaseBackend({ // ... other config functionsDir: path.resolve(__dirname, "../functions"),});Rebase wird:
- Das Verzeichnis nach
.ts- /.js-Dateien durchsuchen - Validieren, dass jeder Standard-Export eine Hono-App ist (Duck-Typing über
.fetch()+.routes) - Jede App unter
/api/functions/<filename>einbinden - Die Auth-Middleware anwenden (siehe Authentifizierung unten)
Funktionen als Werte
Abschnitt betitelt „Funktionen als Werte“Seit 0.25 Unter 0.24 stammen Funktionen nur aus einem Verzeichnis.
Ein Verzeichnis ist eine Möglichkeit, das zu erzeugen, was der Server ausliefert: einen Record vom URL-Segment zur Hono-App. Übergeben Sie diesen Record selbst als functions, oder lesen Sie mit loadFunctions ein Verzeichnis in ihn ein:
import path from "path";import { initializeRebaseBackend, loadFunctions } from "@rebasepro/server";import sendInvoice from "../functions/send-invoice";
await initializeRebaseBackend({ app, server, functions: { "send-invoice": sendInvoice } }); // → /api/functions/send-invoice// the same as functionsDir:await initializeRebaseBackend({ app, server, functions: await loadFunctions(path.resolve(__dirname, "../functions")) });Geben Sie functions oder functionsDir an, nicht beides: Der Boot lehnt das Paar ab. loadFunctions(dir, { onSkip }) übergibt Ihnen jede übersprungene Datei samt Begründung. loadFunctionsFromDirectory und loadFunctionsWithDiagnostics funktionieren weiterhin, sind veraltet (deprecated) und warnen einmalig. Für sich allein lässt sich die Functions-Gruppe an jedem Pfad mit app.route("/hooks", backend.routes.functions()) einbinden, und getUser(c) sowie getDriver(c) funktionieren darin; siehe Eigene Server-Integration.
Dateibenennung und Routen-Mapping
Abschnitt betitelt „Dateibenennung und Routen-Mapping“| Datei | Mount-Pfad |
|---|---|
functions/hello.ts |
/api/functions/hello/* |
functions/send-invoice.ts |
/api/functions/send-invoice/* |
functions/webhooks.ts |
/api/functions/webhooks/* |
Funktionen werden nur auf der obersten Ebene des Verzeichnisses erkannt – es gibt keine Rekursion. functions/admin/users.ts wird zwar von rebase build kompiliert, aber niemals eingebunden; flachen Sie den Namen stattdessen ab (functions/admin-users.ts). Ein Unterverzeichnis wird beim Booten gemeldet und auf dem Listing-Endpunkt gezählt, anstatt stillschweigend ignoriert zu werden.
Dateien, die übersprungen werden:
index.ts/index.js– reserviert*.test.ts/*.test.js– Testdateien*.d.ts– Typdeklarationen- Unterverzeichnisse und
.mts- /.cts- /.tsx- /.jsx- /.mjs- /.cjs-Dateien – werden als Probleme gemeldet, da der Build mehr kompiliert, als die Runtime lädt
Der Name ist auch überall sonst die Identität der Funktion: Er ist das URL-Segment, das Ziel des Scopes functions:invoke:<name>, den ein Schlüssel braucht, um sie aufzurufen, und der Wert, nach dem REBASE_FUNCTIONS_ONLY filtert, wenn Sie einer Funktion einen eigenen Prozess zuweisen.
Export-Formate
Abschnitt betitelt „Export-Formate“Der Loader akzeptiert neben defineFunction zwei Export-Formate:
Hono-App
Abschnitt betitelt „Hono-App“import { Hono } from "hono";import type { HonoEnv } from "@rebasepro/server/functions";
const app = new Hono<HonoEnv>();app.get("/status", (c) => c.json({ ok: true }));export default app;Factory-Funktion
Abschnitt betitelt „Factory-Funktion“import { Hono } from "hono";import type { HonoEnv } from "@rebasepro/server/functions";
export default function () { const app = new Hono<HonoEnv>(); app.get("/status", (c) => c.json({ ok: true })); return app;}defineFunction gibt genau die Hono-App zurück, die diese manuell aufbauen, sodass alle drei austauschbar sind. Es erspart Ihnen die Deklaration von Hono<HonoEnv> und übergibt Ihnen das rebase-Singleton im Callback.
Unter der Haube: Der Duck-Typing-Loader
Abschnitt betitelt „Unter der Haube: Der Duck-Typing-Loader“In einem Monorepo können Ihre Funktionen eine andere Kopie von Hono auflösen als die, die Rebase verwendet; dann ist exported instanceof Hono für einen funktionierenden Router falsch. Der Loader prüft daher stattdessen die Form (isHonoLike): ein Nicht-Null-Objekt mit einer .fetch-Methode und einem .routes-Array.
ES-Modul-Compiler-Escape
Abschnitt betitelt „ES-Modul-Compiler-Escape“Der Loader wandelt jeden Dateipfad in eine Datei-URL um (pathToFileURL(filePath).href), sodass er auf Windows und POSIX gleichermaßen importiert, und ruft import(url) über new Function("url", "return import(url)") auf, damit ein TypeScript-Build es nicht in ein require() umschreiben kann, das unter ESM fehlschlägt.
Authentifizierung und Kontext-Weitergabe
Abschnitt betitelt „Authentifizierung und Kontext-Weitergabe“Benutzerdefinierte Funktionen werden mit der gleichen Auth-Middleware wie die Datenrouten eingebunden, jedoch mit requireAuth: false. Das bedeutet:
- Das JWT des Benutzers wird geparst und in den Kontext injiziert, sofern vorhanden
- Anfragen werden jedoch nicht abgelehnt, wenn kein JWT bereitgestellt wird
- Sie müssen Routen, die eine Authentifizierung erfordern, explizit schützen
Ein Aufrufer, der ein ungültiges Token vorlegt, erreicht Ihren Handler nie: Ein nicht verifizierbares oder abgelaufenes Token wird von der Middleware selbst mit 401 abgelehnt, sodass eine abgelaufene Sitzung niemals stillschweigend auf eine anonyme herabgestuft wird.
Auslesen des Aufrufers
Abschnitt betitelt „Auslesen des Aufrufers“import { defineFunction, getUser, getUserId, getRoles, getScopes, isAdmin } from "@rebasepro/server/functions";
export default defineFunction((app) => { app.get("/me", (c) => { const user = getUser(c); // { uid, roles, ...claims } | undefined if (!user) return c.json({ error: "Unauthorized" }, 401); return c.json({ uid: user.uid, roles: user.roles, admin: isAdmin(c), scopes: getScopes(c) }); });});getUser gibt ein eingegrenztes Objekt zurück: uid ist ein String und roles ist immer ein Array, unabhängig von der vom Aufrufer verwendeten Authentifizierungsmethode. getUserId(c) und getRoles(c) sind Direktaufrufe. getScopes(c) ist alles, was der Aufrufer darf, als Scope-Strings: für eine Person die Datenebene, die eigenen Scopes der App und die Scopes ihrer Rollen; für einen API-Schlüssel genau das, was der Schlüssel hält.
Routen schützen
Abschnitt betitelt „Routen schützen“import { defineFunction, requireAuth, requireAdmin, requireRole, getUserId } from "@rebasepro/server/functions";
export default defineFunction((app) => { // Public endpoint — no guard, so anyone can call it. app.get("/public", (c) => c.json({ message: "Anyone can access this" }));
// 401 for anonymous callers. app.post("/protected", requireAuth, (c) => c.json({ message: `Hello, ${getUserId(c)}` }));
// 401 anonymous, 403 without the admin role. Order matters. app.post("/admin-only", requireAuth, requireAdmin, (c) => c.json({ ok: true }));
// Any one of the named roles. app.post("/publish", requireAuth, requireRole("editor", "admin"), (c) => c.json({ ok: true }));});Platzieren Sie Guards wie oben im eigenen Middleware-Slot der Route statt app.use("/*", requireAuth) zu verwenden. use() deckt nur die Routen ab, die darunter deklariert sind, sodass eine später hinzugefügte Route – am Ende der Datei, Monate später – stillschweigend ungeschützt bleibt.
Das Auslesen von getUser(c) ist kein Guard. Ein anonymer Aufrufer erhält undefined und Ihr Handler wird trotzdem ausgeführt. Nur ein Guard oder ein explizites if (!user) return 401 stoppt die Anfrage.
Scopes und App-Scopes
Abschnitt betitelt „Scopes und App-Scopes“requireAdmin lässt die Rolle admin zu und sonst niemanden. Für eine Aktion, die eine engere Rolle oder ein API-Schlüssel erreichen soll, schützen Sie die Route mit requireScope. Es nimmt einen integrierten Scope oder einen, den die App unter auth.scopes in der Users-Collection deklariert, etwa "project:deploy": { label: "Deploy projects", target: "project" }:
import { defineFunction, requireAuth, requireScope, hasScope } from "@rebasepro/server/functions";
export default defineFunction((app) => { // 403 SCOPE_MISSING unless the caller holds project:deploy, or project:deploy:<this project>. app.post("/:project", requireAuth, requireScope("project:deploy", c => c.req.param("project")), (c) => { return c.json({ deploying: c.req.param("project") }); });
// The same question inside a handler. app.get("/:project/can-deploy", requireAuth, (c) => c.json({ allowed: hasScope(c, "project:deploy", c.req.param("project")) }));});Jede angemeldete Person hält jeden App-Scope, für eine Person entscheidet requireScope also nichts: Prüfen Sie im Handler, ob diese Person dieses Projekt deployen darf. Was der Scope hinzufügt, ist eine Möglichkeit, einen Schlüssel einzuschränken: Ein Schlüssel, der project:deploy:p1 hält, kommt nur für p1 durch. Ein Schlüssel braucht außerdem functions:invoke oder functions:invoke:<name>, bevor er die Funktion überhaupt erreicht. Siehe App-Scopes.
Service-Key-Authentifizierung
Abschnitt betitelt „Service-Key-Authentifizierung“Rebase unterstützt einen statischen REBASE_SERVICE_KEY, der in Ihrer .env für Skripte oder Server-zu-Server-Aufrufe definiert ist.
Wenn eine externe Anfrage den Service-Schlüssel über den Authorization-Header (Authorization: Bearer <service_key>) übergibt, führt die Auth-Middleware automatisch Folgendes aus:
- Validiert den Schlüssel mittels zeitkonstantem Vergleich, um Timing-Angriffe zu verhindern.
- Gewährt Zugriff auf Admin-Ebene und setzt den Aufrufer auf
{ uid: "service", roles: ["admin"] }. - Injiziert einen
DataDriver, der auf dieselbe Service-Identität beschränkt ist. Row-Level Security gilt weiterhin – sie wird als{ uid: "service", roles: ["admin"] }ausgewertet und nicht übersprungen.
Interne Selbst-Authentifizierung
Abschnitt betitelt „Interne Selbst-Authentifizierung“Wenn Sie keinen REBASE_SERVICE_KEY konfiguriert haben, generiert Rebase einen zufälligen internen Schlüssel pro Systemstart. Das rebase-Singleton verwendet diesen Schlüssel automatisch beim Aufruf der servereigenen Control-Plane-APIs (wie rebase.auth oder rebase.storage). Das bedeutet, dass Ihre serverseitige Logik auch ohne manuell konfigurierten Service-Schlüssel jederzeit administrative Aufgaben ausführen kann.
Zugriff auf Datenbank und Services
Abschnitt betitelt „Zugriff auf Datenbank und Services“1. Der benutzerbezogene Driver – für alles, was eine Anfrage bedient
Abschnitt betitelt „1. Der benutzerbezogene Driver – für alles, was eine Anfrage bedient“getDriver(c) gibt den Treiber beschränkt auf den Aufrufer zurück, sodass jeder Lese- und Schreibzugriff anhand Ihrer Row-Level-Security-Richtlinien als dieser Benutzer ausgewertet wird:
import { defineFunction, requireAuth, requireDriver } from "@rebasepro/server/functions";
export default defineFunction((app) => { app.get("/", requireAuth, async (c) => { const driver = requireDriver(c); const myProducts = await driver.fetchCollection({ path: "products", limit: 10 }); return c.json(myProducts); });});requireDriver(c) ist getDriver(c) ohne das ! – es wirft einen Fehler mit dem konkreten Verbindungsproblem, anstatt zwanzig Zeilen später an undefined zu scheitern.
2. rebase.dataAsAdmin – für vertrauenswürdige Hintergrundarbeiten
Abschnitt betitelt „2. rebase.dataAsAdmin – für vertrauenswürdige Hintergrundarbeiten“import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.post("/:id/approve", requireAuth, requireAdmin, async (c) => { const id = c.req.param("id"); await rebase.dataAsAdmin.collection<Record<string, unknown>>("jobs").update(id, { status: "published", approved_at: new Date().toISOString(), }); return c.json({ success: true }); });});rebase.dataAsAdmin ist untypisiert, bis Sie Ihre generierte Database registrieren.
RLS-bezogener Driver vs. Rebase-Singleton
Abschnitt betitelt „RLS-bezogener Driver vs. Rebase-Singleton“getDriver(c) (request-scoped) |
rebase.dataAsAdmin (service identity) |
|
|---|---|---|
| Ausführung als | Der Aufrufer (uid, dessen Rollen) |
{ uid: "service", roles: ["admin"] } |
| RLS-Erzwingung | ✅ Ja (ausgewertet für den Aufrufer) | ✅ Ja (ausgewertet für die Service-Identität) |
| Ideal für… | Allgemeine Benutzer-CRUD, Suche und Abfragen | Hintergrund-Jobs, System-Trigger, Webhooks |
| API-Stil | Methoden auf Driver-Ebene (fetchCollection, save) |
Fluent-Collection-Zugriffsmethoden (rebase.dataAsAdmin.jobs.find) |
Was dataAsAdmin genau ist
Abschnitt betitelt „Was dataAsAdmin genau ist“rebase.dataAsAdmin ist auf Admin-Ebene beschränkt, umgeht RLS jedoch nicht. Der Driver wird einmalig beim Booten mit withAuth({ uid: "service", roles: ["admin"] }) initialisiert, sodass jeder Lese- und Schreibzugriff innerhalb einer Transaktion ausgeführt wird, die zur eingeschränkten Rolle rebase_user mit app.uid = 'service' gewechselt hat. Ihre Richtlinien werden ausgewertet – und zwar gegen diese Identität.
Für die meisten Projekte wird dieser Unterschied nie sichtbar, da die Standardrichtlinien, die Rebase in jede Collection einfügt, serverContext() OR rolesOverlap(['admin']) zulassen und die Service-Identität den zweiten Zweig erfüllt. Er wird in dem Moment relevant, in dem Sie eigene Richtlinien schreiben:
policy.serverContext()ist dafür false. Dieser Helper kompiliert zurebase.uid() IS NULL, und dieuiddieses Accessors ist'service'. Eine Collection mitdisableDefaultPolicies: true, deren einzige SchreibregelserverContext()ist, verweigert einendataAsAdmin-Schreibvorgang mit dem Postgres-Fehler42501, und ein Lesevorgang auf einer solchen Collection liefert null Zeilen mit HTTP 200 – der stille Fehlerfall. Schreiben SierolesOverlap(["admin"])(oder fügen Sie es hinzu), wenn Sie „mein Backend“ meinen.- Seine Reichweite entspricht der Reichweite eines
admin-Benutzers. Wenn Sie einem Anwendungsbenutzer die Rolleadmingewähren, erhält er genau die Zeilen, die auch dieser Accessor sieht. Es ist kein privater Kanal.
3. rebase.sql() – Raw SQL und der einzige reine Node-Accessor
Abschnitt betitelt „3. rebase.sql() – Raw SQL und der einzige reine Node-Accessor“Wenn Sie wirklich eine bedingungslose Umgehung benötigen, ist rebase.sql() die Lösung: Raw SQL über die Owner-Verbindung, keine Richtlinien, jede Zeile. Es ist das am stärksten privilegierte Element im Kontext einer Funktion – mehr noch als der Accessor mit „admin“ im Namen.
import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.get("/stats", requireAuth, requireAdmin, async (c) => { const rows = await rebase.sql( "SELECT count(*) AS total FROM jobs WHERE status = $1", { params: ["published"] } ); return c.json({ totalJobs: Number(rows[0]?.total ?? 0) }); });});Es läuft über eine TCP-Verbindung zu Ihrer Datenbank, was es zum einzigen Accessor macht, der an einen Node-Prozess gebunden ist. Das hat bei heutigen Deployments keinerlei Nachteile – es ist lediglich der eine Punkt, den man wissen muss, falls eine Funktion später migriert werden soll. Siehe Laufzeit-Portabilität.
Konfiguration und Secrets
Abschnitt betitelt „Konfiguration und Secrets“Lesen Sie Konfigurationen innerhalb des Handlers aus, niemals auf Modulebene:
import { defineFunction, requireEnv, lazyResource } from "@rebasepro/server/functions";
// Built once, on the first request that needs it — not at import time.const apiKey = lazyResource((env) => env.PRICING_API_KEY ?? "");
export default defineFunction((app) => { app.get("/price", async (c) => { const endpoint = requireEnv(c, "PRICING_API_URL"); const response = await fetch(endpoint, { headers: { authorization: `Bearer ${apiKey(c)}` } }); return c.json(await response.json()); });});Warum dies auf jeder Laufzeitumgebung wichtig ist, einschließlich Node:
// Don't. If STRIPE_SECRET_KEY is unset, this throws while the file is being// imported — and the loader reports that as a *skipped function*. The route// 404s, with the reason buried in a boot log line.const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);Ein Lesezugriff auf Modulebene wird ausgewertet, wenn die Datei importiert wird – bevor überhaupt eine Anfrage existiert. Unter Node bedeutet dies, dass eine einzige fehlende Variable die gesamte Datei und jede darin enthaltene Route lahmlegt. Bei einem Host, der die Konfiguration an den Request statt an den Prozess bindet, gibt es beim Importzeitpunkt überhaupt nichts auszulesen.
getEnv(c)– jede für diese Anfrage sichtbare Variableenv(c, "NAME")– eine Variable, getrimmt; Leerzeichen gelten als nicht gesetztrequireEnv(c, "NAME")– dasselbe, wirft jedoch einen Fehler mit dem Namen der VariablenlazyResource(factory)– erstellt einen aufwendigen Client einmalig bei der ersten Verwendung
rebase doctor meldet process.env-Lesezugriffe auf Modulebene in Ihrem functions-Verzeichnis.
Hintergrundarbeiten
Abschnitt betitelt „Hintergrundarbeiten“Aufgaben, die über die Antwort hinaus fortgeführt werden sollen, gehören in waitUntil:
import { defineFunction, requireAuth, waitUntil } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.post("/orders", requireAuth, async (c) => { const order = await c.req.json(); // The caller does not wait for this, but shutdown does. waitUntil(c, rebase.email.send({ to: "warehouse@example.com", subject: "New order", html: "<p>Pick and pack</p>" })); return c.json({ received: true }); });});Ein Promise ohne await sieht gleichwertig aus, ist es aber nicht. waitUntil bringt zwei entscheidende Vorteile:
- Unter Node wird das Promise nachverfolgt, sodass ein Graceful Shutdown darauf wartet, anstatt dass der Prozess während eines halb gesendeten Webhooks beendet wird. Ein nicht abgewartetes Promise geht bei
SIGTERMeinfach verloren. - Auf einem Isolate-basierten Host wird der Host angewiesen, das Isolate am Leben zu erhalten, bis das Promise abgeschlossen ist. Ohne dies wird die Aufgabe in dem Moment abgebrochen, in dem die Antwort aufgelöst wird – stillschweigend und mit einem sauberen 200-Status in den Logs.
Eine Zurückweisung wird protokolliert, anstatt dem Handler für unbehandelte Rejections überlassen zu werden, sodass bei einem Fehler die Route benannt wird, von der er stammt.
Laufzeit-Portabilität
Abschnitt betitelt „Laufzeit-Portabilität“Eine benutzerdefinierte Funktion ist eine Hono-App, und Hono läuft auf jeder JavaScript-Server-Laufzeitumgebung. Ob Ihre Funktion woanders als in einem Node-Prozess ausgeführt werden kann, hängt daher davon ab, was ihre eigene Datei importiert und verwendet.
Nichts davon stellt eine Einschränkung für das dar, was Sie heute schreiben können. Jedes Rebase-Deployment ist ein Node-Prozess, eine Funktion, die eine Datei liest oder einen Socket öffnet, ist vollkommen valide, und kein Build oder Deployment schlägt deswegen fehl. Dies wird hier festgehalten, damit die Antwort jetzt bereits bekannt ist und nicht später für jede Datei einzeln herausgefunden werden muss.
Portabel – funktioniert auf jeder Runtime:
- Alles, was aus
@rebasepro/server/functionsexportiert wird getDriver(c)undrebase.dataAsAdmin– beide laufen über dieselbe Verbindung, unabhängig davon, wo sie ausgeführt werdenrebase.auth,rebase.storage,rebase.emailfetch,Request/Response,URL,crypto.subtle,TextEncoder– die Web-Plattform- Jede Abhängigkeit, die kein Node benötigt
Node-only:
rebase.sql()– die Datenbank-Owner-Verbindung ist ein TCP-Socket- Ein direkt importierter Drizzle-/
pg-/mongodb-Client aus demselben Grund - Node-Built-ins:
fs,path,crypto(das Node-Modul –globalThis.cryptoist portabel),child_process, … - Pakete, die darauf aufbauen:
jsonwebtoken,nodemailer,sharp,bcrypt, …
Latente Fehler auf jeder Runtime – diese sollten unabhängig davon behoben werden:
- Lesezugriff auf
process.envauf Modulebene (siehe Konfiguration und Secrets) - Fire-and-Forget-Promises anstelle von
waitUntil - Sich darauf verlassen, dass ein Handler nach einem Timeout des Requests weiterläuft. Unter Node tut er das; dies ist eine Eigenschaft des Prozesses, kein Garantieversprechen des Frameworks
Eigene Funktionen prüfen
Abschnitt betitelt „Eigene Funktionen prüfen“rebase build gibt für jeden handlungsrelevanten Befund eine Zeile aus und erfasst das Ergebnis pro Funktion im Bundle-Manifest:
{ "functions": [ { "name": "hello", "file": "backend/functions/hello.js", "portable": true }, { "name": "reports", "file": "backend/functions/reports.js", "portable": false, "requires": ["imports the Node built-in \"fs\""] } ]}rebase doctor meldet dasselbe, ohne zu bauen.
Wenn Sie einen runtime-spezifischen Pfad benötigen
Abschnitt betitelt „Wenn Sie einen runtime-spezifischen Pfad benötigen“runtimeKey() gibt "node", "workerd", "deno", "bun", "edge-light", "fastly" oder "other" zurück; isNodeRuntime() ist die gängige Prüfung. Verwenden Sie sie für ein Graceful Degrading, nicht um eine Implementierung aufzuspalten – eine Funktion, die zwei Implementierungen erfordert, sind zwei Funktionen.
import { defineFunction, isNodeRuntime } from "@rebasepro/server/functions";
export default defineFunction((app, { rebase }) => { app.get("/stats", async (c) => { if (!isNodeRuntime()) return c.json({ error: "Not available here" }, 501); const rows = await rebase.sql("SELECT count(*) AS total FROM jobs"); return c.json({ totalJobs: Number(rows[0]?.total ?? 0) }); });});Reihenfolge der Routenregistrierung
Abschnitt betitelt „Reihenfolge der Routenregistrierung“Benutzerdefinierte Funktionen werden geladen und eingebunden, nachdem initializeRebaseBackend() das Core-Setup abgeschlossen hat. Die Initialisierungsreihenfolge lautet:
- Bootstrapper – Datenbankverbindungen, Auth-Tabellen, Realtime-Services
- Auth-Routen –
/api/auth/*,/api/admin/* - Storage-Routen –
/api/storage/* - Daten-Routen –
/api/data/*(CRUD für Collections) - Benutzerdefinierte Funktionen ←
/api/functions/* - Cronjobs –
/api/admin/cron/* - WebSocket – Realtime-Abonnements
Das bedeutet, dass Ihre benutzerdefinierten Funktionen Zugriff auf alle initialisierten Dienste haben. Registrieren Sie Routen, die vor Rebase ausgeführt werden müssen, direkt auf der Hono-App vor dem Aufruf von initializeRebaseBackend():
const app = new Hono<HonoEnv>();
// This runs BEFORE Rebase routesapp.get("/health", (c) => c.json({ status: "ok" }));
// Rebase initialization — registers all /api/* routesconst instance = await initializeRebaseBackend({ app, /* ... */ });Beispiel: Webhook-Handler
Abschnitt betitelt „Beispiel: Webhook-Handler“import { defineFunction, requireEnv, waitUntil, lazyResource } from "@rebasepro/server/functions";
/** Constructed on the first request, from that request's configuration. */const secret = lazyResource((env) => env.STRIPE_WEBHOOK_SECRET ?? "");
export default defineFunction((app, { rebase }) => { // Deliberately public: Stripe has no token to send. The signature is the // authentication, so verify it before doing anything else. app.post("/", async (c) => { const signature = c.req.header("stripe-signature"); const body = await c.req.text();
if (!signature || !verifySignature(body, signature, secret(c))) { return c.json({ error: "Bad signature" }, 400); }
const event = JSON.parse(body) as { type: string; data: { object: Record<string, string> } };
if (event.type === "checkout.session.completed") { const session = event.data.object; await rebase.dataAsAdmin.collection("subscriptions").create({ user_id: session.client_reference_id, stripe_id: session.subscription, status: "active", }); // Fulfilment can outlive the response; the 200 tells Stripe to stop retrying. waitUntil(c, notifyFulfilment(requireEnv(c, "FULFILMENT_URL"), session)); }
return c.json({ received: true }); });});
declare function verifySignature(body: string, signature: string, secret: string): boolean;declare function notifyFulfilment(url: string, session: Record<string, string>): Promise<void>;Debugging
Abschnitt betitelt „Debugging“Wenn eine Funktion erfolgreich geladen wurde, sehen Sie:
⚡ Loaded function route: helloWenn das Laden fehlschlägt, liefert der Loader Diagnoseausgaben:
[functions] broken-function.ts: default export is not a Hono app or factory. Skipping. export type: object (SomeClass) prototype methods: constructor, someMethod Hint: ensure the function exports a Hono app created with the same hono version as the server.Der Router wird für das Verzeichnis eingebunden, nicht für die darin enthaltenen Funktionen. Wenn der Import jeder einzelnen Datei fehlschlägt – eine fehlende Umgebungsvariable auf Modulebene reicht aus, um alle lahmzulegen –, antwortet GET /api/functions dennoch mit 200 und einer leeren Liste plus einem skipped-Zähler, sodass „nichts geladen“ von „dieser Build enthielt keine Funktionen“ unterschieden werden kann. Das Listing selbst erfordert einen angemeldeten Aufrufer, einen API-Schlüssel, der den einfachen Scope functions:invoke hält, oder den Service-Schlüssel – die Funktionen bleiben für jeden aufrufbar, den die jeweilige Funktion zulässt, aber die Übersicht über sie ist nicht öffentlich. Die Gründe verbleiben im Boot-Log.
Timeouts und Rate-Limits
Abschnitt betitelt „Timeouts und Rate-Limits“Für /api/functions/* gelten zwei Obergrenzen:
- Request-Timeout – standardmäßig 30 Sekunden, beantwortet mit
504und dem CodeFUNCTION_TIMEOUT. Konfigurierbar überfunctionsTimeoutMs(oderREBASE_FUNCTIONS_TIMEOUT_MS);0deaktiviert es. Der Handler kann von außen nicht gestoppt werden, bekommt aber Bescheid: Seit 0.25getSignal(c)aus@rebasepro/server/functionswird im Moment des 504 abgebrochen, übergeben Sie es also anfetchund alles andere, was ein Signal akzeptiert (fetch(url, { signal: getSignal(c) })). Arbeit, der das Signal nicht übergeben wurde, läuft weiter – das Timeout gibt den Client und den Socket frei, nicht die Arbeit. Dass der Handler nach dem 504 weiterläuft, ist eine Eigenschaft eines langlebigen Node-Prozesses, keine Zusage des Vertrags; alles, was zwingend abgeschlossen werden muss, gehört inwaitUntil. - Rate-Limit – API-Schlüssel und angemeldete Aufrufer teilen sich die Buckets der Daten-API. Anonyme Aufrufer erhalten ein eigenes, deutlich großzügigeres Kontingent (3000 pro Zeitfenster), da dieser Router standardmäßig für Webhook-Empfänger öffentlich ist. Überschreiben Sie dies mit
rateLimit.anonymousFunctions;nullschaltet es aus.
Unbehandelte Promise-Rejections werden protokolliert und sind nicht fatal: Ein Fire-and-Forget-Aufruf in einer Funktion würde andernfalls den gesamten Prozess beenden. Setzen Sie REBASE_EXIT_ON_UNHANDLED_REJECTION=1, um das Standardverhalten von Node zu aktivieren.
Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- Backend-Übersicht – Vollständige Referenz zur Backend-Konfiguration
- Entity-Callbacks – Logik bei Datenänderungen ausführen
- Cronjobs – Geplante Hintergrundaufgaben