Skip to content

Procedures

SpacetimeDB: Procedures ↗

Procedures are declared in callable groups with params and a return value type.

import * as Effect from "effect/Effect"
import * as Stdb from "effect-spacetimedb"
const user = Stdb.table("user", {
public: true,
columns: {
id: Stdb.string().primaryKey(),
name: Stdb.string(),
},
})
const Users = Stdb.StdbGroup.make("Users").add(
Stdb.StdbFn.procedure("user_get", {
params: Stdb.struct({ id: Stdb.string() }),
returns: Stdb.option(user.row),
}),
)
const Module = Stdb.StdbModule.make("app").addTables(user).add(Users)
const { Db, Tx } = Module
const UsersLive = Stdb.StdbBuilder.group(Module, "Users", {
user_get: Effect.fn(function* ({ id }) {
const tx = yield* Tx
return yield* tx.run(
Effect.gen(function* () {
const db = yield* Db
return yield* db.user.id.find(id)
}),
)
}),
})

Procedure handlers cannot yield Db directly. Use the module’s Tx runner and keep transaction bodies pure database work because the native runtime may retry them after optimistic commit conflicts.

A procedure is the only place a module can make an outbound request. Yield the module’s Http accessor outside any transaction — the host refuses a request while a transaction is open, so fetch first, then open the transaction that records the result.

const { Http, Tx, Db } = Module
const syncLive = Stdb.StdbBuilder.group(Module, "Sync", {
sync_profile: Effect.fn(function* ({ userId }) {
const http = yield* Http
const response = yield* http.fetch(`https://example.test/users/${userId}`)
const profile = yield* http.json<{ readonly name: string }>(response)
const tx = yield* Tx
return yield* tx.run(
Effect.gen(function* () {
const db = yield* Db
yield* db.user.userId.replace({ userId, name: profile.name })
}),
)
}),
})

Failures arrive as StdbHostCallError in the error channel, tagged with the operation (http.fetch, http.json, and so on).

Two host policies decide whether a request is allowed at all, and both are the operator’s, not the module’s:

  • Egress filtering. Requests to loopback, link-local, and private or otherwise special-purpose addresses are refused — 127.0.0.1, 10/8, 172.16/12, 192.168/16, the IPv6 equivalents, and the rest of the special-purpose registry. Ordinary server builds enforce this; only a purpose-built test binary relaxes it for loopback. A module cannot reach a service on its own host or elsewhere inside a private network. Where a hostname resolves to both a blocked and a routable address, the routable one is used.
  • A global off switch. A standalone operator can disable module-originated HTTP entirely with [module-http] enabled = false in the server’s config.toml. It defaults to enabled and takes a server restart to change. With it off, every request fails with module outbound HTTP requests are disabled.

Requests time out after 30 seconds unless you pass a shorter one, and the host clamps any requested timeout to 180 seconds.

Scheduled reducers and procedures are declared with the callable group API and backed by a scheduled table. The scheduled endpoint receives { data: table.row }; insert future work with the generated table .schedule(row) helper from a reducer or transaction body.

import * as Effect from "effect/Effect"
import * as Stdb from "effect-spacetimedb"
const reminderSchedule = Stdb.scheduledTable("reminderSchedule", {
columns: {
note: Stdb.string(),
},
})
const Reminders = Stdb.StdbGroup.make("Reminders").add(
Stdb.StdbFn.scheduledProcedure("reminderFire", {
table: reminderSchedule,
allowExternalCallers: false,
}),
)
const Module = Stdb.StdbModule.make("app")
.addTables(reminderSchedule)
.add(Reminders)
const { Db } = Module
const RemindersLive = Stdb.StdbBuilder.group(Module, "Reminders", {
reminderFire: Effect.fn(function* ({ data }) {
yield* Effect.logInfo(`reminder: ${data.note}`)
}),
})
const enqueueReminder = Effect.gen(function* () {
const db = yield* Db
yield* db.reminderSchedule.schedule({
scheduledAt: Stdb.ScheduleAt.interval("5 minutes"),
note: "wake up",
})
})

Use Stdb.ScheduleAt.interval(...) for persistent re-fire rows, Stdb.ScheduleAt.at(...) for one-shot absolute times, Stdb.ScheduleAt.after(ctx.timestamp, duration) for one-shot relative work, and Stdb.ScheduleAt.nextCron(cron, ctx.timestamp) for cron-derived one-shots. See Scheduled Tables for the table side of the contract, the host’s dispatch semantics, and why a scheduled procedure that does external work between transactions has to claim its work rows in the transaction that selects them.