8 Commits

Author SHA1 Message Date
joshua-spacetime eef0e423a8 Add primary key support for procedural views to rust and ts modules (#5111)
# Description of Changes

Adds support for primary keys to procedural views in rust and typescript
modules. Now clients can receive update events when subscribed to such
views.

```rust
#[spacetimedb::view(accessor = my_players, public, primary_key = id)]
pub fn my_players(ctx: &spacetimedb::ViewContext) -> Vec<Player> {
    ctx.db.players().owner().filter(ctx.sender()).collect()
}
```

```ts
const Player = t.row('Player', {
  id: t.u64().primaryKey(),
  owner: t.identity().index('btree'),
  name: t.string(),
});

export const my_players = spacetimedb.view(
  { public: true },
  t.array(players.rowType),
  ctx => Array.from(ctx.db.players.owner.filter(ctx.sender))
);
```

In rust, a view's primary key is declared within the `#[view]` macro,
whereas in typescript it is declared at the column/row level as it is
for tables. I could have made it a view level attribute for typescript
as well, but since primary keys are already row-level attributes in
typescript, I just kept that as is.

Note, care must be taken to ensure that the view never returns duplicate
primary keys, or else it will fail which will currently result in the
transaction that triggered the view refresh to be rolled back. Better
error handling for this exact scenario will be added in a separate
follow up patch.

## Alternative Considered: #[primary_key] attribute on `SpacetimeType`s
in Rust

This would be equivalent to what we do in typescript.

# API and ABI breaking changes

None, although adding a primary key to an existing view will require a
client update.

# Expected complexity level and risk

3

# Testing

Compile-time failure scenarios:
- [x] Primary key on non-existent column
- [x] Primary key doesn't reference custom accessor name (rust)
- [x] Multiple primary key columns specified (typescript)
- [x] Primary keys must be `FilterableValue`s

SDK tests (rust sdk, rust and ts modules):
- [x] `OnUpdate`
- [x] `OnUpdate` is sender-scoped
- [X] Can join on primary key columns

Smoketests:
- [x] Modifying primary key columns breaks clients (auto-migrations)
2026-06-04 18:44:58 +00:00
Phoebe Goldman 5c04860649 Implement HTTP handlers / webhooks in Rust modules (#4636)
# Description of Changes

Adds support to Rust modules and the SpacetimeDB host for defining HTTP
handlers and registering them to routes.

## User-facing API

In a Rust module, users can annotate functions with the new macro
`#[spacetimedb::http::handler]`. A function annotated this way must
accept exactly two arguments, of types `&mut
spacetimedb::http::HandlerContext` and `spacetimedb::http::Request`
(which is a type alias for `http::Request<spacetimedb::http::Body>`. It
must also return `spacetimedb::http::Response` (which is a type alias
for `http::Response<spacetimedb::http::Body>`).

Once the user has defined an HTTP handler, they can register it to a
route by annotating a function with `#[spacetimedb::http::router]`. Such
a function must take no arguments and return a
`spacetimedb::http::Router`. (The original design put this annotation on
a `static` variable rather than a function, but that turned out to be
undesirable because it required that constructing a `Router` be
`const`.) `Router` exposes various methods for registering handlers to
routes.

All of a database's user-defined routes are exposed under
`/v1/database/:name_or_identity/route/{*path}`.

## Example

See [the new
smoketest](https://github.com/clockworklabs/SpacetimeDB/blob/phoebe/http-handlers-webhooks/crates/smoketests/tests/smoketests/http_routes.rs)
for a more exhaustive example.

A simpler example, which stores arbitrary byte data in a table via a
`POST` request, returns an ID, and then retrieves that same data via a
`GET` request with a query parameter:

```rust
#[spacetimedb::table(accessor = data)]
struct Data {
    #[primary_key]
    #[auto_inc]
    id: u64,
    body: Vec<u8>,
}

#[spacetimedb::http::handler]
fn insert(ctx: &mut HandlerContext, request: Request) -> Response {
    let body: Vec<u8> = request.into_body().into_bytes().into();
    let id = ctx.with_tx(|tx| tx.db.data().insert(Data { id: 0, body: body.clone() }).id);
    Response::new(Body::from_bytes(format!("{id}")))
}

#[spacetimedb::http::handler]
fn retrieve(ctx: &mut HandlerContext, request: Request) -> Response {
    let id = request
        .uri()
        .query()
        .and_then(|query| query.strip_prefix("id="))
        .and_then(|id| u64::from_str(id).ok())
        .unwrap();
    let body = ctx.with_tx(|tx| tx.db.data().id().find(id).map(|data| data.body));
    if let Some(body) = body {
        Response::new(Body::from_bytes(body))
    } else {
        Response::builder().status(404).body(Body::empty()).unwrap()
    }
}

#[spacetimedb::http::router]
fn router() -> Router {
    Router::new().post("/insert", insert).get("/retrieve", retrieve)
}
```

## Design and implementation notes

- As mentioned above, the router is registered via a function, not a
`static` or `const` item. This is because `static` or `const`
initializers must be `const`, and it turns out to be a pain to make all
of the `Router` constructors be `const fn`s.
- The `#[handler]` macro clobbers the original function name with a
`const` variable of type `HttpHandler`. This is unfortunate, but AFAICT
necessary, 'cause we need to pass the string identifier for the handler
to the `Router`, not the function pointer, and Rust allows no (stable
and reliable) way to get a unique string identifier out of a function
item/value, nor to attach data or implement traits for function
items/values. The alternative(s) would involve changing the signature of
the `Router` methods to have uglier and more complex callsites, e.g.
like `.get("/retrieve", retrieve::handler())`, `.get("/retrieve",
handler!(retrieve))` or `.get::<retrieve>("/retrieve")`. I believe that
registering handlers will be much more common than calling their
functions, so I've chosen to make it so that registering them gets the
convenient syntax, even though the inability to call them directly will
be somewhat surprising.
- I haven't wired up energy handling or timing metrics for handler
execution to anywhere. Procedures are still in the same boat.
- HTTP requests to user-defined routes bypass the usual SpacetimeDB auth
middleware, meaning that the host does not validate (or inspect in any
way) `Authorization` headers in requests before invoking the
user-defined handler. This is required to allow arbitrary
user-programmable handling of `Authorization` headers, including those
in formations which SpacetimeDB would reject. As a result of this,
`HandlerContext` doesn't expose a `sender` or `sender_connection_id`.
- HTTP route paths may consist only of a very restrictive set of
characters. I've chosen this set to keep our options open in the future
to add additional syntax to routes, like for registering wildcard
segments and path parameters:
  - ASCII digits.
  - ASCII letters.
  - `-_~/`.
- The internal data structure that represents a `Router` is currently a
`Vec<Route>`, meaning that resolving a request to a route is
`O(num_routes)`. Registering a route checks against each previous route
for uniqueness, meaning that constructing a router is `O(num_routes ^
2)`. There are TODO comments to use a trie, but I think this can wait,
as I expect most databases to register few routes.
- Commit 999a7c317 contains a fix to a mostly-unrelated bug where a few
bindings introduced by the SATS derive macros were unhygienic and not in
a reserved namespace, leading to name conflicts. I discovered this
'cause I tried writing an HTTP handler named `index` to serve the
index/root of a website and it broke.

## Still TODO

- [x] Resolve various TODO comments in the diff.
- [x] Documentation.
- [x] C# bindings support.
- [x] C++ bindings support.
- [x] V8 host support.
- [x] TypeScript bindings support.

# API and ABI breaking changes

New APIs, currently flagged as `unstable`, which will eventually need
stability guarantees. No (intentional) breaking changes, or changes to
existing APIs at all.

# Expected complexity level and risk

3? Changes to our HTTP routing to support the user-defined routes.

# Testing

<!-- Describe any testing you've done, and any testing you'd like your
reviewers to do,
so that you're confident that all the changes work as expected! -->

- [x] New smoketest of the behavior!
- [x] I dunno, maybe try hosting a simple webpage and see how it works?
- [x] Build a test app with Stripe integration.
  - @aasoni did this.

---------

Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Co-authored-by: Jason Larabie <jason@clockworklabs.io>
2026-05-29 16:06:15 +00:00
Alessandro Asoni 0f4dbea81a Revert recursive mounts from module def (#5098)
# Description of Changes

Revert recursive module mounts in schema def. This appears to be causing
unintended issues for language bindings.
The intention was for this to not cause any issues, but it appears to be
problematic when adding new enum veriants to the def sections (e.g. for
procedures).

# API and ABI breaking changes

No

# Expected complexity level and risk

Reverting to a previous version

# Testing

<!-- Describe any testing you've done, and any testing you'd like your
reviewers to do,
so that you're confident that all the changes work as expected! -->

- [ ] <!-- maybe a test you want to do -->
- [ ] <!-- maybe a test you want a reviewer to do, so they can check it
off when they're satisfied. -->
2026-05-22 15:44:27 +00:00
Alessandro Asoni c416055bc4 Add recursive mounts for submodules [Namespaces Part 1] (#4990)
# Description of Changes

Add a recursive `mounts` fields to ModuleDef that references submodules.

# API and ABI breaking changes

This should not be a breaking change. However new V10 modules that are
built with this change will not be publishable to older hosts as the new
field won't be recognized.

# Expected complexity level and risk

2

# Testing

Mostly tested using ci tests. Will be tested more thoroughly in the
typescript PR that will use the new mounts field.
2026-05-13 20:21:27 +00:00
clockwork-labs-bot dd2c95f113 Move CaseConversionPolicy to public SpacetimeDB namespace (#4382)
Move `CaseConversionPolicy` from `SpacetimeDB.Internal` to the public
`SpacetimeDB` namespace so module authors can write:

```csharp
[SpacetimeDB.Settings]
public const CaseConversionPolicy CASE_CONVERSION_POLICY = CaseConversionPolicy.SnakeCase;
```

instead of the verbose:

```csharp
public const SpacetimeDB.Internal.CaseConversionPolicy CASE_CONVERSION_POLICY = SpacetimeDB.Internal.CaseConversionPolicy.SnakeCase;
```

### Changes
- Move enum definition from `SpacetimeDB.Internal` to `SpacetimeDB`
namespace in autogen
- Fully qualify all `Internal` references to
`SpacetimeDB.CaseConversionPolicy`
- Codegen source generator accepts both
`SpacetimeDB.CaseConversionPolicy` and
`SpacetimeDB.Internal.CaseConversionPolicy` for backward compatibility
- Updated test fixture and verified snapshots
- All 4 codegen tests pass

---------

Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
2026-02-20 22:21:46 +00:00
Shubham Mishra 124808c2ff Case conversion Raw Def changes (#4294)
# Description of Changes

Case conversion addtion to `RawModuleDefV10`.
Chunk has been taken from -
https://github.com/clockworklabs/SpacetimeDB/pull/4264 to unblock
csharp, and ts modules.

# API and ABI breaking changes
NA
2026-02-13 22:27:29 +00:00
Shubham Mishra bd3748b5f0 RLS section in RawModuleDefV10. (#4149)
# Description of Changes
Keep RLS in `RawModuleDefV10` for now, revisit replacing it with Views
before 2.0 release.

<!-- Please describe your change, mention any related tickets, and so on
here. -->

# API and ABI breaking changes
NA

# Expected complexity level and risk1
1
2026-01-29 04:01:10 +00:00
Shubham Mishra fd3cdb3d0b [2/3] [Rust] Host changes for RawModuleDefV10 (#4105)
# Description of Changes

Host side changes for Wasm to work with `RawModuleDefV10` along with
prior versions.
Based on #https://github.com/clockworklabs/SpacetimeDB/pull/4098

# API and ABI breaking changes
NA

# Expected complexity level and risk
1.5

# Testing
Existing test will cover for any regression.
Not yet functional for  `RawModuleDefV10`, so can not be tested now.
2026-01-28 15:10:57 +00:00