# bevy_stdb
A [Bevy](https://bevy.org/) integration for [SpacetimeDB](https://spacetimedb.com).
[](https://crates.io/crates/bevy_stdb)
[](https://docs.rs/bevy_stdb)

_Please enjoy this useless AI generated image based on the README contents of this repo._
## Overview
`bevy_stdb` adapts SpacetimeDB's connection and callback model into Bevy-style resources, systems, plugins, and messages. Its built around a few core ideas:
- Configure everything through `StdbPlugin`
- Expose the active live connection as a Bevy resource via `StdbConnection`
- Forward table callbacks into Bevy messages with `TableRegistrar`
- Store subscription intent independently from the live connection with `StdbSubscriptions`
- Optionally retry failed connections with `StdbReconnectOptions`
The library is organized around connection-scoped lifecycle concerns:
- **connection lifecycle**: establish the initial connection, expose the active connection resource, and track connection state
- **table lifecycle**: initialize table message channels once and re-bind table callbacks whenever a connection becomes active
- **subscription lifecycle**: store desired subscription intent and re-apply queued subscriptions when connected
- **reconnect lifecycle**: optionally retry connection attempts after disconnects using configurable backoff
## Features
- **Builder-style setup** via `StdbPlugin`
- **Connection resource** access through `StdbConnection`
- **Table event bridging** into normal Bevy `Message`s
- **Managed subscription intent** through `StdbSubscriptions`
- **Optional reconnect support** through `StdbReconnectOptions`
## Example
```rust
use bevy::prelude::*;
use bevy_stdb::prelude::*;
use crate::module_bindings::{DbConnection, RemoteModule};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum MySubKey {
PlayerInfo,
}
pub type StdbConn = StdbConnection<DbConnection>;
pub type StdbSubs = StdbSubscriptions<MySubKey, RemoteModule>;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(
StdbPlugin::<DbConnection, RemoteModule>::default()
.with_module_name("my_module")
.with_uri("http://localhost:3000")
.with_tables(|reg, db| {
reg.table(&db.player_info());
reg.table_without_pk(&db.nearby_monsters());
})
.with_subscriptions::<MySubKey>(|subs| {
// This is a great place to add "global" subscriptions,
// but you can subscribe from any system too.
subs.subscribe_query(MySubKey::PlayerInfo, |q| q.from.player_info());
})
.with_reconnect(StdbReconnectOptions::default())
.with_run_fn(DbConnection::run_threaded),
)
.add_systems(Update, on_player_info_insert)
.run();
}
fn on_player_info_insert(mut msgs: ReadInsertMessage<PlayerInfo>) {
for msg in msgs.read() {
info!("player inserted: {:?}", msg.row);
}
}
```
## Table registration
Use `StdbPlugin::with_tables` to register all table callbacks in one place.
The closure receives both a `TableRegistrar` and the current database view. Use the `db` argument to select tables, views, and event streams, then register them through `reg`.
Typical forms are:
```rust
.with_tables(|reg, db| {
reg.table(&db.player_info());
reg.table_without_pk(&db.nearby_monsters());
reg.event_table(&db.some_event_stream());
reg.view(&db.some_view());
})
```
`with_tables` is intended to be called once during plugin setup. These registrations are replayed during initialization to install the required Bevy message channels and again whenever the connection enters the connected state to bind callbacks for the current live database view.
## Messages
Depending on the table shape, `bevy_stdb` forwards updates into Bevy messages such as:
- `InsertMessage<T>`
- `DeleteMessage<T>`
- `UpdateMessage<T>`
- `InsertUpdateMessage<T>`
This lets normal Bevy systems react to database changes using message readers.
## Subscriptions
`StdbSubscriptions` stores desired subscription intent separately from the live connection.
That means you can:
- declare global (or any other) subscriptions during plugin setup with `with_subscriptions`
- queue additional subscriptions later from normal Bevy systems
- automatically re-apply queued subscription intent after reconnect
Like the other `StdbPlugin` configuration methods, `with_subscriptions` is intended to be configured once during plugin setup.
Subscriptions are keyed, so the application can refer to them using its own domain-specific identifiers, allowing you to also unsubscribe as needed later.
## Reconnects
Reconnect behavior is opt-in.
Use `StdbPlugin::with_reconnect` with `StdbReconnectOptions` to enable retry behavior after disconnects. Like the other `StdbPlugin` configuration methods, it is intended to be configured once during plugin setup. When reconnect succeeds:
- the live `StdbConnection` resource is replaced
- table callbacks are re-bound
- queued subscriptions are re-applied
## Type Aliases
It is useful to define some type aliases of your own. I suggest doing something like this:
```rust
#[derive(Clone, Eq, Hash, PartialEq, Debug)]
pub enum SubKeys {
PlayerInfo,
TimeOfDay,
}
pub type StdbConn = StdbConnection<DbConnection>;
pub type StdbSubs = StdbSubscriptions<SubKeys, RemoteModule>;
// Or a more constrained version for typical use cases:
// pub type StdbConn<'w> = Res<'w, StdbConnection<DbConnection>>;
// pub type StdbSubs<'w> = ResMut<'w, StdbSubscriptions<SubKeys, RemoteModule>>;
// Usage example
fn example_system(conn: Res<StdbConn>, mut subs: ResMut<StdbSubs>) {
let my_table = conn.db().player_info().id().find(&1);
subs.subscribe_query(SubKeys::TimeOfDay, |q| q.from.world_clock());
}
```
## Compatibility
| 0.1 - 0.2 | 0.18 | 2.0 |
| 0.3 | 0.18 | 2.1 |
## Notes
This crate focuses on table-driven client workflows. Reducer and procedure access still exist through the active `StdbConnection`, but the primary Bevy-facing event flow is table/message based.
The project was heavily inspired by [`bevy_spacetimedb`](https://docs.rs/bevy_spacetimedb/), but takes a different approach to plugin structure, connection lifecycle handling, reconnect behavior, and subscription restoration.