diff --git a/docs/docs/01-intro/01-overview.md b/docs/docs/01-intro/01-overview.md
index 2e6c90ab9d9..089f37e786d 100644
--- a/docs/docs/01-intro/01-overview.md
+++ b/docs/docs/01-intro/01-overview.md
@@ -289,6 +289,124 @@ or at a specific time.
+### Procedure
+
+A **procedure** is a function exported by a [database](#database), similar to a [reducer](#reducer).
+Connected [clients](#client-side-sdks) 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.
+
+Procedures are currently in beta, and their API may change in upcoming SpacetimeDB releases.
+
+
+
+
+Because procedures are unstable, Rust modules that define them must opt in to the `unstable` feature in their `Cargo.toml`:
+
+```toml
+[dependencies]
+spacetimedb = { version = "1.x", features = ["unstable"] }
+```
+
+Then, that module can define a procedure:
+
+```rust
+#[spacetimedb::procedure]
+pub fn make_request(ctx: &mut spacetimedb::ProcedureContext) -> String {
+ // ...
+}
+```
+
+And a Rust [client](#client) can call that procedure:
+
+```rust
+fn main() {
+ // ...setup code, then...
+ ctx.procedures.make_request();
+}
+```
+
+A Rust [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value:
+
+```rust
+fn main() {
+ // ...setup code, then...
+ ctx.procedures.make_request_then(|ctx, res| {
+ match res {
+ Ok(string) => log::info!("Procedure `make_request` returned {string}"),
+ Err(e) => log::error!("Procedure `make_request` failed! {e:?}"),
+ }
+ })
+}
+```
+
+
+
+
+C# modules currently cannot define procedures. Support for defining procedures in C# modules will be released shortly.
+
+A C# [client](#client) can call a procedure defined by a Rust or TypeScript module:
+
+```csharp
+void Main()
+{
+ // ...setup code, then...
+ ctx.Procedures.MakeRequest();
+}
+```
+
+A C# [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value:
+
+```csharp
+void Main()
+{
+ // ...setup code, then...
+ ctx.Procedures.MakeRequestThen((ctx, res) =>
+ {
+ if (res.IsSuccess)
+ {
+ Log.Debug($"Procedure `make_request` returned {res.Value!}");
+ }
+ else
+ {
+ throw new Exception($"Procedure `make_request` failed: {res.Error!}");
+ }
+ });
+}
+```
+
+
+
+
+A procedure can be defined in a TypeScript module:
+
+```typescript
+spacetimedb.procedure("make_request", t.string(), ctx => {
+ // ...
+})
+```
+
+And a TypeScript [client](#client) can call that procedure:
+
+```typescript
+ctx.procedures.makeRequest();
+```
+
+A Rust [client](#client) can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value:
+
+```typescript
+ctx.procedures.makeRequest().then(
+ res => console.log(`Procedure make_request returned ${res}`),
+ err => console.error(`Procedure make_request failed! ${err}`),
+);
+```
+
+
+
+
+See [Procedures](/procedures) for more details about procedures.
+
### Client
A **client** is an application that connects to a [database](#database). A client logs in using an [identity](#identity) and receives an [connection id](#connectionid) to identify the connection. After that, it can call [reducers](#reducer) and query public [tables](#table).
diff --git a/docs/docs/11-Procedures/01-overview.md b/docs/docs/11-Procedures/01-overview.md
new file mode 100644
index 00000000000..f8acb7887e1
--- /dev/null
+++ b/docs/docs/11-Procedures/01-overview.md
@@ -0,0 +1,513 @@
+---
+Title: Overview
+slug: /procedures
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Procedures - Overview
+
+A **procedure** is a function exported by a [database](/#database), similar to a [reducer](/#reducer).
+Connected [clients](/#client-side-sdks** 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
+
+
+
+
+Because procedures are unstable, Rust modules that define them must opt in to the `unstable` feature in their `Cargo.toml`:
+
+```toml
+[dependencies]
+spacetimedb = { version = "1.*", features = ["unstable"] }
+```
+
+Define a procedure by annotating a function with `#[spacetimedb::procedure]`.
+
+This function's first argument must be of type `&mut spacetimedb::ProcedureContext`.
+By convention, this argument is named `ctx`.
+
+A procedure may accept any number of additional arguments.
+Each argument must be of a type that implements `spacetimedb::SpacetimeType`.
+When defining a `struct` or `enum`, annotate it with `#[derive(spacetimedb::SpacetimeType)]`
+to make it usable as a procedure argument.
+These argument values will not be broadcast to clients other than the caller.
+
+A procedure may return a value of any type that implements `spacetimedb::SpacetimeType`.
+This return value will be sent to the caller, but will not be broadcast to any other clients.
+
+```rust
+#[spacetimedb::procedure]
+fn add_two_numbers(ctx: &mut spacetimedb::ProcedureContext, lhs: u32, rhs: u32) -> u64 {
+ lhs as u64 + rhs as u64
+}
+```
+
+
+
+
+Support for procedures in C# modules is coming soon!
+
+
+
+
+Define a procedure with `spacetimedb.procedure`:
+
+```typescript
+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 `ProcedureContext::with_tx`.
+
+```rust
+#[spacetimedb::table(name = my_table)]
+struct MyTable {
+ a: u32,
+ b: String,
+}
+
+#[spacetimedb::procedure]
+fn insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) {
+ ctx.with_tx(|ctx| {
+ ctx.my_table().insert(MyTable { a, b });
+ });
+}
+```
+
+`ProcedureContext::with_tx` takes a function of type `Fn(&TxContext) -> T`.
+Within that function, the `&TxContext` can be used to access the database
+[in all the same ways as a `ReducerContext`](https://docs.rs/spacetimedb/latest/spacetimedb/struct.ReducerContext.html).
+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 panics, the transaction will be rolled back, and its changes will be discarded.
+However, for transactions that may fail,
+[prefer calling `try_with_tx` and returning a `Result`](#fallible-database-operations) rather than panicking.
+
+:::warning
+The function passed to `ProcedureContext::with_tx` 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 `with_tx`.
+:::
+
+
+
+
+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`.
+
+```typescript
+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`](/modules/typescript#reducercontext)
+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, instead use `ProcedureContext::try_with_tx`:
+
+```rust
+#[spacetimedb::procedure]
+fn maybe_insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) {
+ ctx.try_with_tx(|ctx| {
+ if a < 10 {
+ return Err("a is less than 10!");
+ }
+ ctx.my_table().insert(MyTable { a, b });
+ Ok(())
+ });
+}
+```
+
+`ProcedureContext::try_with_tx` takes a function of type `Fn(&TxContext) -> Result`.
+If the function returns `Ok`, the transaction will be committed,
+and its changes to the database state will become permanent and be broadcast to clients.
+If that function returns `Err`, the transaction will be rolled back, and its changes will be discarded.
+
+
+
+
+For fallible database operations, you can throw an error inside the transaction function:
+
+```typescript
+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
+[`ProcedureContext::with_tx`](#accessing-the-database) and [`ProcedureContext::try_with_tx`](#fallible-database-operations)
+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.
+
+```rust
+#[spacetimedb::table(name = player)]
+struct Player {
+ id: spacetimedb::Identity,
+ level: u32,
+}
+
+#[spacetimedb::procedure]
+fn find_highest_level_player(ctx: &mut ProcedureContext) {
+ let highest_level_player = ctx.with_tx(|ctx| {
+ ctx.db.player().iter().max_by_key(|player| player.level)
+ });
+ match highest_level_player {
+ Some(player) => log::info!("Congratulations to {}", player.id),
+ None => log::warn!("No players..."),
+ }
+}
+```
+
+
+
+
+Functions passed to
+[`ProcedureCtx.withTx`](#accessing-the-database)
+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.
+
+```typescript
+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.get` performs simple `GET` requests with no headers:
+
+```rust
+#[spacetimedb::procedure]
+fn get_request(ctx: &mut ProcedureContext) {
+ match ctx.http.get("https://example.invalid") {
+ Ok(response) => {
+ let (response, body) = response.into_parts();
+ log::info!(
+ "Got response with status {} and body {}",
+ response.status,
+ body.into_string_lossy(),
+ )
+ },
+ Err(error) => log::error!("Request failed: {error:?}"),
+ }
+}
+```
+
+`ctx.http.send` sends any [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html)
+whose body can be converted to `spacetimedb::http::Body`.
+`http::Request` is re-exported as `spacetimedb::http::Request`.
+
+```rust
+#[spacetimedb::procedure]
+fn post_request(ctx: &mut spacetimedb::ProcedureContext) {
+ let request = spacetimedb::http::Request::builder()
+ .uri("https://example.invalid/upload")
+ .method("POST")
+ .header("Content-Type", "text/plain")
+ .body("This is the body of the HTTP request")
+ .expect("Building `Request` object failed");
+ match ctx.http.send(request) {
+ Ok(response) => {
+ let (response, body) = response.into_parts();
+ log::info!(
+ "Got response with status {} and body {}",
+ response.status,
+ body.into_string_lossy(),
+ )
+ }
+ Err(error) => log::error!("Request failed: {error:?}"),
+ }
+}
+```
+
+Each of these methods returns a [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html#method.body)
+containing a `spacetimedb::http::Body`. `http::Response` is re-exported as `spacetimedb::http::Response`.
+
+Set a timeout for a `ctx.http.send` request by including a `spacetimedb::http::Timeout` as an [`extension`](https://docs.rs/http/latest/http/request/struct.Builder.html#method.extension):
+
+```rust
+#[spacetimedb::procedure]
+fn get_request_with_short_timeout(ctx: &mut spacetimedb::ProcedureContext) {
+ let request = spacetimedb::http::Request::builder()
+ .uri("https://example.invalid")
+ .method("GET")
+ // Set a timeout of 10 ms.
+ .extension(spacetimedb::http::Timeout(std::time::Duration::from_millis(10).into()))
+ // Empty body for a `GET` request.
+ .body(())
+ .expect("Building `Request` object failed");
+ ctx.http.send(request).expect("HTTP request failed");
+}
+```
+
+Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database).
+
+
+
+
+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:
+
+```typescript
+#[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:
+
+```typescript
+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](#accessing-the-database).
+
+
+
+
+## Calling procedures
+
+
+
+
+Clients can invoke procedures using methods on `ctx.procedures`:
+
+```rust
+ctx.procedures.insert_a_value(12, "Foo".to_string());
+```
+
+
+
+
+Clients can invoke procedures using methods on `ctx.Procedures`:
+
+```csharp
+ctx.Procedures.InsertAValue(12, "Foo");
+```
+
+
+
+
+Clients can invoke procedures using methods on `ctx.procedures`:
+
+```typescript
+ctx.procedures.insertAValue({ a: 12, b: "Foo" });
+```
+
+
+
+
+### Observing return values
+
+
+
+
+A client can also invoke a procedure while registering a callback to run when it completes.
+That callback will have access to the return value of the procedure,
+or an error if the procedure fails.
+
+```rust
+ctx.procedures.add_two_numbers_then(1, 2, |ctx, result| {
+ let sum = result.expect("Procedure failed");
+ println!("1 + 2 = {sum}");
+});
+```
+
+
+
+
+A client can also invoke a procedure while registering a callback to run when it completes.
+That callback will have access to the return value of the procedure,
+or an error if the procedure fails.
+
+```csharp
+ctx.Procedures.AddTwoNumbers(12, "Foo", (ctx, result) =>
+{
+ if (result.IsSuccess)
+ {
+ Log.Info($"1 + 2 = {result.Value!}");
+ }
+ else
+ {
+ throw result.Error!;
+ }
+});
+```
+
+
+
+
+When a client invokes a procedure, it gets a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which resolves to the return value of the procedure.
+
+```typescript
+ctx.procedures.addTwoNumbers({ lhs: 1, rhs: 2 }).then(
+ sum => console.log(`1 + 2 = ${sum}`)
+);
+```
+
+
+
diff --git a/docs/docs/11-how-to/01-incremental-migrations.md b/docs/docs/12-how-to/01-incremental-migrations.md
similarity index 100%
rename from docs/docs/11-how-to/01-incremental-migrations.md
rename to docs/docs/12-how-to/01-incremental-migrations.md
diff --git a/docs/docs/11-how-to/02-reject-client-connections.md b/docs/docs/12-how-to/02-reject-client-connections.md
similarity index 100%
rename from docs/docs/11-how-to/02-reject-client-connections.md
rename to docs/docs/12-how-to/02-reject-client-connections.md
diff --git a/docs/docs/11-how-to/03-using-auth-claims.md b/docs/docs/12-how-to/03-using-auth-claims.md
similarity index 100%
rename from docs/docs/11-how-to/03-using-auth-claims.md
rename to docs/docs/12-how-to/03-using-auth-claims.md
diff --git a/docs/docs/11-how-to/_category_.json b/docs/docs/12-how-to/_category_.json
similarity index 100%
rename from docs/docs/11-how-to/_category_.json
rename to docs/docs/12-how-to/_category_.json
diff --git a/docs/docs/12-spacetimeauth/01-overview.md b/docs/docs/13-spacetimeauth/01-overview.md
similarity index 100%
rename from docs/docs/12-spacetimeauth/01-overview.md
rename to docs/docs/13-spacetimeauth/01-overview.md
diff --git a/docs/docs/12-spacetimeauth/02-creating-a-project.md b/docs/docs/13-spacetimeauth/02-creating-a-project.md
similarity index 100%
rename from docs/docs/12-spacetimeauth/02-creating-a-project.md
rename to docs/docs/13-spacetimeauth/02-creating-a-project.md
diff --git a/docs/docs/12-spacetimeauth/03-configuring-a-project.md b/docs/docs/13-spacetimeauth/03-configuring-a-project.md
similarity index 100%
rename from docs/docs/12-spacetimeauth/03-configuring-a-project.md
rename to docs/docs/13-spacetimeauth/03-configuring-a-project.md
diff --git a/docs/docs/12-spacetimeauth/04-testing.md b/docs/docs/13-spacetimeauth/04-testing.md
similarity index 100%
rename from docs/docs/12-spacetimeauth/04-testing.md
rename to docs/docs/13-spacetimeauth/04-testing.md
diff --git a/docs/docs/12-spacetimeauth/05-react-integration.md b/docs/docs/13-spacetimeauth/05-react-integration.md
similarity index 100%
rename from docs/docs/12-spacetimeauth/05-react-integration.md
rename to docs/docs/13-spacetimeauth/05-react-integration.md
diff --git a/docs/docs/13-http-api/01-authorization.md b/docs/docs/14-http-api/01-authorization.md
similarity index 100%
rename from docs/docs/13-http-api/01-authorization.md
rename to docs/docs/14-http-api/01-authorization.md
diff --git a/docs/docs/13-http-api/02-identity.md b/docs/docs/14-http-api/02-identity.md
similarity index 100%
rename from docs/docs/13-http-api/02-identity.md
rename to docs/docs/14-http-api/02-identity.md
diff --git a/docs/docs/13-http-api/03-database.md b/docs/docs/14-http-api/03-database.md
similarity index 100%
rename from docs/docs/13-http-api/03-database.md
rename to docs/docs/14-http-api/03-database.md
diff --git a/docs/docs/13-http-api/_category_.json b/docs/docs/14-http-api/_category_.json
similarity index 100%
rename from docs/docs/13-http-api/_category_.json
rename to docs/docs/14-http-api/_category_.json
diff --git a/docs/docs/14-internals/01-module-abi-reference.md b/docs/docs/15-internals/01-module-abi-reference.md
similarity index 100%
rename from docs/docs/14-internals/01-module-abi-reference.md
rename to docs/docs/15-internals/01-module-abi-reference.md
diff --git a/docs/docs/14-internals/02-sats-json.md b/docs/docs/15-internals/02-sats-json.md
similarity index 100%
rename from docs/docs/14-internals/02-sats-json.md
rename to docs/docs/15-internals/02-sats-json.md
diff --git a/docs/docs/14-internals/03-bsatn.md b/docs/docs/15-internals/03-bsatn.md
similarity index 100%
rename from docs/docs/14-internals/03-bsatn.md
rename to docs/docs/15-internals/03-bsatn.md
diff --git a/docs/docs/15-appendix/index.md b/docs/docs/16-appendix/index.md
similarity index 100%
rename from docs/docs/15-appendix/index.md
rename to docs/docs/16-appendix/index.md