Skip to main content

Functions API

Serverless functions are your app's backend logic: sending email, taking payments, calling AI, or anything custom Roc writes for you. The @instroc/functions package invokes them from the frontend, and this page also covers the inbox helper sendMessage from @instroc/data, since it is the right tool for contact and feedback forms and needs no function at all.

useFunction

useFunction(name) returns an invoke function plus state:

const { invoke, loading, error, lastResult } = useFunction("send_email");

invoke(payload?) never throws. It always resolves to an InvokeResult, and your code must branch on result.success:

{
success: boolean;
data?: any; // the function's return value on success
error?: string; // what went wrong on failure
duration_ms?: number;
}

loading is true while a call is in flight, error is a string with the last failure, and lastResult holds the most recent InvokeResult. Because invoke cannot throw, there is no try/catch to write; the failure path is just the else branch. This is the opposite of data mutations, which do throw on failure, so do not carry the try/catch habit from one to the other.

const result = await invoke({ to: "[email protected]" });
if (result.success) {
// use result.data
} else {
// show result.error
}

useFunctions

useFunctions() lists the functions available in your project, which is useful for admin screens and debugging.

When functions run

A function has one of three triggers. An http function runs when your app invokes it (through the SDK or the HTTP API). A scheduled function runs on a cron schedule you set in Cloud under Scheduled Tasks. A webhook function runs when an outside service posts to its webhook URL; see Webhooks for the receiving side.

Template functions

Three ready-made templates cover the most common needs. Each depends on a connected integration, managed on the Integrations panel (see Integrations):

  • send_email sends outbound email and needs an email integration connected (Resend or SendGrid).
  • process_payment runs payments through your connected Stripe account: one-time checkout, subscriptions, or a payment intent for custom flows. Confirmed payments land automatically in your app's private payments table, so useQuery("payments") is the entitlement check.
  • ai_call calls an AI model and needs an OpenAI or Anthropic API key connected.

Key-based integrations (email and AI) require a Pro or Business plan; Stripe is available on every tier. Integration calls are billed to your Instroc Cloud balance, not AI credits.

Calling functions over HTTP

External callers can invoke any http-triggered function directly:

curl -X POST "https://your-app.example/api/baas/{projectId}/functions/send_email/invoke" \
-H "Authorization: Bearer <project key>" \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]"}'

See the API overview for the base URL and response envelope.

sendMessage: the built-in inbox

For contact forms, feedback boxes, and waitlists you do not need a function or an email integration at all. sendMessage from @instroc/data delivers the message to your app's Inbox, and you are emailed automatically, free.

sendMessage({
body: string; // required, 1 to 5000 characters
from_name?: string;
from_email?: string;
metadata?: object;
}) // resolves to { delivered, id?, reason? }

Two behaviors to know. If the matching inbox channel is switched off, the result is delivered: false with reason: "channel_disabled"; treat that as a soft success and still thank the visitor. And when the sender is signed in, their account email is stamped on the message server-side, so a forged from_email cannot impersonate another user.

Example: a contact form

import { useState } from "react";
import { sendMessage } from "@instroc/data";

function ContactForm() {
const [body, setBody] = useState("");
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);

async function submit(e: React.FormEvent) {
e.preventDefault();
await sendMessage({ body, from_email: email });
setSent(true); // channel_disabled is a soft success, still thank them
}

if (sent) return <p>Thanks, we will get back to you soon.</p>;

return (
<form onSubmit={submit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your email"
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="How can we help?"
required
/>
<button type="submit">Send</button>
</form>
);
}

Example: a checkout button

Checkout must open in a new tab, because the payment page will not render inside the preview frame, and users must be signed in before starting checkout.

import { useFunction } from "@instroc/functions";
import { useAuth } from "@instroc/auth";

function BuyButton() {
const { user } = useAuth();
const { invoke, loading } = useFunction("process_payment");

async function checkout() {
// Payload fields depend on how Roc set up your products.
const result = await invoke({ product: "pro-plan" });
if (result.success) {
window.open(result.data.url, "_blank"); // the checkout link
} else {
// show result.error
}
}

if (!user) return <p>Sign in to purchase.</p>;

return (
<button onClick={checkout} disabled={loading}>
{loading ? "Starting checkout..." : "Buy now"}
</button>
);
}

After payment, check entitlement anywhere in the app with useQuery("payments") from the Data API.