Skip to main content

Procedures

A procedure is a function exported by a database, similar to a reducer. Connected clients can call procedures. Procedures can perform additional operations not possible in reducers, including making HTTP requests to external services. However, procedures don't automatically run in database transactions, and must manually open and commit a transaction in order to read from or modify the database state. For this reason, prefer defining reducers rather than procedures unless you need to use one of the special procedure operators.

warning

Procedures are currently in beta, and their API may change in upcoming SpacetimeDB releases.

Defining Procedures

Define a procedure with spacetimedb.procedure:

spacetimedb.procedure(
    "add_two_numbers",
    { lhs: t.u32(), rhs: t.u32() },
    t.u64(),
    (ctx, { lhs, rhs }) => BigInt(lhs) + BigInt(rhs),
);

The spacetimedb.procedure function takes:

  • the procedure name,
  • (optional) an object representing its parameter types,
  • its return type,
  • and the procedure function itself.

The function will receive a ProcedureContext and an object of its arguments, and it must return a value corresponding to its return type. This return value will be sent to the caller, but will not be broadcast to any other clients.

Accessing the database

Unlike reducers, procedures don't automatically run in database transactions. This means there's no ctx.db field to access the database. Instead, procedure code must manage transactions explicitly with ProcedureCtx.withTx.

const MyTable = table(
    { name: "my_table" },
    {
        a: t.u32(),
        b: t.string(),
    },
)

const spacetimedb = schema(MyTable);

#[spacetimedb::procedure]
spacetimedb.procedure("insert_a_value", { a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) => {
    ctx.withTx(ctx => {
        ctx.myTable.insert({ a, b });
    });
    return {};
})

ProcedureCtx.withTx takes a function of (ctx: TransactionCtx) => T. Within that function, the TransactionCtx can be used to access the database in all the same ways as a ReducerCtx When the function returns, the transaction will be committed, and its changes to the database state will become permanent and be broadcast to clients. If the function throws an error, the transaction will be rolled back, and its changes will be discarded.

warning

The function passed to ProcedureCtx.withTx may be invoked multiple times, possibly seeing a different version of the database state each time.

If invoked more than once with reference to the same database state, it must perform the same operations and return the same result each time.

If invoked more than once with reference to different database states, values observed during prior runs must not influence the behavior of the function or the calling procedure.

Avoid capturing mutable state within functions passed to withTx.

Fallible database operations

For fallible database operations, you can throw an error inside the transaction function:

spacetimedb.procedure("maybe_insert_a_value", { a: t.u32(), b: t.string() }, t.unit(), (ctx, { a, b }) => {
    ctx.withTx(ctx => {
        if (a < 10) {
            throw new SenderError("a is less than 10!");
        }
        ctx.myTable.insert({ a, b });
    });
})

Reading values out of the database

Functions passed to ProcedureCtx.withTx may return a value, and that value will be returned to the calling procedure.

Transaction return values are never saved or broadcast to clients, and are used only by the calling procedure.

const Player = table(
    { name: "player" },
    {
        id: t.identity(),
        level: t.u32(),
    },
);

const spacetimedb = schema(Player);

spacetimedb.procedure("find_highest_level_player", t.unit(), ctx => {
    let highestLevelPlayer = ctx.withTx(ctx =>
        Iterator.from(ctx.db.player).reduce(
            (a, b) => a == null || b.level > a.level ? b : a,
            null
        )
    );
    if (highestLevelPlayer != null) {
        console.log("Congratulations to ", highestLevelPlayer.id);
    } else {
        console.warn("No players...");
    }
    return {};
});

HTTP Requests

Procedures can make HTTP requests to external services using methods contained in ctx.http.

ctx.http.fetch is similar to the browser fetch() API, but is synchronous.

It can perform simple GET requests:

#[spacetimedb::procedure]
spacetimedb.procedure("get_request", t.unit(), ctx => {
    try {
        const response = ctx.http.fetch("https://example.invalid");
        const body = response.text();
        console.log(`Got response with status ${response.status} and body ${body}`);
    } catch (e) {
        console.error("Request failed: ", e);
    }
    return {};
});

It can also accept an options object to specify a body, headers, HTTP method, and timeout:

spacetimedb.procedure("post_request", t.unit(), ctx => {
    try {
        const response = ctx.http.fetch("https://example.invalid/upload", {
            method: "POST",
            headers: { "Content-Type": "text/plain" },
            body: "This is the body of the HTTP request",
        });
        const body = response.text();
        console.log(`Got response with status ${response.status} and body {body}`);
    } catch (e) {
        console.error("Request failed: ", e);
    }
    return {};
});

spacetimedb.procedure("get_request_with_short_timeout", t.unit(), ctx => {
    try {
        const response = ctx.http.fetch("https://example.invalid", {
            method: "GET",
            timeout: TimeDuration.fromMillis(10),
        });
        const body = response.text();
        console.log(`Got response with status ${response.status} and body {body}`);
    } catch (e) {
        console.error("Request failed: ", e);
    }
    return {};
});

Procedures can't send requests at the same time as holding open a transaction.

Calling procedures

Clients can invoke procedures using methods on ctx.procedures:

ctx.procedures.insertAValue({ a: 12, b: "Foo" });

Observing return values

When a client invokes a procedure, it gets a Promise which resolves to the return value of the procedure.

ctx.procedures.addTwoNumbers({ lhs: 1, rhs: 2 }).then(
    sum => console.log(`1 + 2 = ${sum}`)
);

Example: Calling an External AI API

A common use case for procedures is integrating with external APIs like OpenAI's ChatGPT. Here's a complete example showing how to build an AI-powered chat feature.

import { schema, t, table, SenderError } from 'spacetimedb/server';

const AiMessage = table(
  { name: 'ai_message', public: true },
  {
    user: t.identity(),
    prompt: t.string(),
    response: t.string(),
    createdAt: t.timestamp(),
  }
);

const spacetimedb = schema(AiMessage);

spacetimedb.procedure(
  'ask_ai',
  { prompt: t.string(), apiKey: t.string() },
  t.string(),
  (ctx, { prompt, apiKey }) => {
    // Make the HTTP request to OpenAI
    const response = ctx.http.fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model: 'gpt-4',
        messages: [{ role: 'user', content: prompt }],
      }),
    });

    if (response.status !== 200) {
      throw new SenderError(`API returned status ${response.status}`);
    }

    const data = response.json();
    const aiResponse = data.choices?.[0]?.message?.content;

    if (!aiResponse) {
      throw new SenderError('Failed to parse AI response');
    }

    // Store the conversation in the database
    ctx.withTx(txCtx => {
      txCtx.db.aiMessage.insert({
        user: txCtx.sender,
        prompt,
        response: aiResponse,
        createdAt: txCtx.timestamp,
      });
    });

    return aiResponse;
  }
);

Calling from a client

// Call the procedure and wait for the AI response
const response = await ctx.procedures.askAi({
  prompt: "What is SpacetimeDB?",
  apiKey: process.env.OPENAI_API_KEY,
});

console.log("AI says:", response);
warning

Security note: Never hardcode API keys in your client code. Consider storing them securely on the server side or using environment variables during development.