From f6b337e233850ec7df4bee9e0fdc128519e9abf0 Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Wed, 8 Jul 2026 15:16:09 -0400
Subject: [PATCH 1/8] updates
---
.../00200-quickstarts/00155-nuxt.md | 2 +
.../00200-quickstarts/00180-browser.md | 6 +-
.../00300-tutorials/00100-chat-app.md | 10 ++-
.../00100-databases/00500-cheat-sheet.md | 3 +
.../00600-clients/00200-codegen.md | 12 +--
.../00700-typescript-reference.md | 18 +++-
.../00100-how-to/00400-row-level-security.md | 83 +++++++++++++++++++
docs/static/llms.md | 4 +-
skills/csharp-client/SKILL.md | 6 +-
skills/typescript-client/SKILL.md | 8 +-
skills/typescript-server/SKILL.md | 10 ++-
skills/unity/SKILL.md | 2 +-
skills/unreal/SKILL.md | 2 +-
13 files changed, 145 insertions(+), 21 deletions(-)
diff --git a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
index f64bffae21a..22081d55523 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
@@ -170,6 +170,8 @@ export default defineEventHandler(async () => {
The root `app.vue` wraps your app in a `SpacetimeDBProvider` that manages the WebSocket connection. The provider is wrapped in `ClientOnly` so it only runs in the browser, while SSR uses the server API route for initial data.
+
+ The token saved to `localStorage` is tied to this browser and cannot be recovered if lost, so it is recommended for development only. For production, use an external [OIDC provider](../../00200-core-concepts/00500-authentication.md).
```vue
diff --git a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
index b1bb250cf11..c2c2c165a9b 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
@@ -56,6 +56,10 @@ npm run build
:::tip
The browser IIFE bundle also exposes the generated `tables` query builders, so you can use query-builder subscriptions here too.
:::
+
+ :::warning
+ The token saved to `localStorage` is tied to this browser and cannot be recovered if lost, so it is recommended for development only. For production, use an external OIDC provider. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+ :::
```html
@@ -97,7 +101,7 @@ npm run build
```javascript
// Call a reducer with named arguments
-conn.reducers.add({ name: 'Alice' });
+conn.reducers.add({ name: 'Alice' }).catch(console.error);
```
diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
index 8bf81f5b1c1..44163d0e403 100644
--- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
+++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
@@ -1444,6 +1444,10 @@ Here we are configuring our SpacetimeDB connection by specifying the server URI,
We are also using `localStorage` to store our SpacetimeDB credentials. This way, we can reconnect to SpacetimeDB with the same `Identity` and token if we refresh the page. The first time we connect, we won't have any credentials stored, so we pass `undefined` to the `withToken` method. This will cause SpacetimeDB to generate new credentials for us.
+:::warning
+Identities issued this way are tied to a single token. If that token is lost, there is no way to recover or re-authenticate as that identity, so this approach is recommended for **development only**. For production applications, use an external OIDC provider. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+:::
+
If you chose a different name for your database, replace `quickstart-chat` with that name, or republish your module as `quickstart-chat`.
Our React hooks will subscribe to the data in SpacetimeDB. When we subscribe, SpacetimeDB will run our subscription queries and store the result in a local "client cache". This cache will be updated in real-time as the data in the table changes on the server.
@@ -1532,7 +1536,7 @@ Modify the `onSubmitNewName` callback by adding a call to the `setName` reducer:
const onSubmitNewName = (e: React.FormEvent) => {
e.preventDefault();
setSettingName(false);
- setName({ name: newName });
+ setName({ name: newName }).catch(console.error);
};
```
@@ -1542,11 +1546,11 @@ Next, modify the `onSubmitMessage` callback by adding a call to the `sendMessage
const onSubmitMessage = (e: React.FormEvent) => {
e.preventDefault();
setNewMessage('');
- sendMessage({ text: newMessage });
+ sendMessage({ text: newMessage }).catch(console.error);
};
```
-SpacetimeDB generated these functions for us based on the type information provided by our module. Calling these functions will invoke our reducers in our module.
+SpacetimeDB generated these functions for us based on the type information provided by our module. Calling these functions will invoke our reducers in our module. They return a `Promise` that rejects if the reducer fails, so we log any error to the console.
Let's try out our app to see the result of these changes.
diff --git a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
index 884c5117f5a..55b95444cff 100644
--- a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
+++ b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
@@ -878,6 +878,9 @@ LOG_DEBUG("Debug: " + msg);
7. **Use `spacetimedb/server`** — For modules, use `spacetimedb/server` (builder API), not `spacetimedb-sdk` (client decorators).
8. **`t.object` not `t.struct`** — Use `t.object('Name', {...})` for product types.
9. **`autoInc` not `autoIncrement`** — Use `.autoInc()` on column builders.
+10. **Row fields are camelCase** — Generated client row fields convert snake_case column names: `trip_id` → `row.tripId`.
+11. **Await reducer calls** — Client reducer calls return `Promise` that rejects with `SenderError` on failure; `await` or `.catch()` them.
+12. **Throw `SenderError` in reducers** — A plain `Error` surfaces as an internal error, not a reducer error.
diff --git a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
index 4da763eba32..f380a574a68 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md
@@ -107,7 +107,7 @@ For example, a `user` table becomes:
export default __t.row({
id: __t.u64(),
name: __t.string(),
- email: __t.string(),
+ emailAddress: __t.string(),
});
// Access via DbConnection
@@ -123,7 +123,7 @@ public partial class User
{
public ulong Id;
public string Name;
- public string Email;
+ public string EmailAddress;
}
// Access via DbConnection
@@ -138,7 +138,7 @@ conn.Db.User
pub struct User {
pub id: u64,
pub name: String,
- pub email: String,
+ pub email_address: String,
}
// Access via DbConnection
@@ -162,7 +162,7 @@ struct FUser
FString Name;
UPROPERTY(BlueprintReadWrite)
- FString Email;
+ FString EmailAddress;
};
// Access via DbConnection
@@ -172,6 +172,8 @@ Context.Db->User
+Note that generated names follow each language's conventions, including field names: a column declared as `email_address` in your module becomes `emailAddress` in TypeScript and `EmailAddress` in C#.
+
See the [Tables](../00300-tables.md) documentation for details on defining tables in your module.
### Reducers
@@ -189,7 +191,7 @@ For example, a `create_user` reducer becomes:
```typescript
// Call the reducer
-conn.reducers.createUser({ name, email });
+await conn.reducers.createUser({ name, email });
// Register a callback to observe reducer invocations
conn.reducers.onCreateUser((ctx, { name, email }) => {
diff --git a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
index d1252bed5e5..6f1b6eeecad 100644
--- a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
+++ b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md
@@ -992,7 +992,7 @@ The `onUpdate` callback runs whenever an already-resident row in the client cach
Only handles with a declared primary key expose `onUpdate` callbacks. Handles for tables or views without a primary key will not have `onUpdate` or `removeOnUpdate` methods. TypeScript view handles expose `onUpdate` callbacks when the returned row type has exactly one column marked with `.primaryKey()`.
-The `Row` type will be an autogenerated type which matches the row type defined by the module.
+The `Row` type will be an autogenerated type which matches the row type defined by the module. Its field names are the module's column names converted to camelCase, so a column declared `trip_id` appears as `tripId`.
`removeOnUpdate` may be used to un-register a previously-registered `onUpdate` callback.
@@ -1010,10 +1010,24 @@ All [`DbContext`](#interface-dbcontext) implementors, including [`DbConnection`]
Each reducer defined by the module has three methods on the `.reducers`:
-- An invoke method, whose name is the reducer's name converted to camel case, like `setName`. This requests that the module run the reducer.
+- An invoke method, whose name is the reducer's name converted to camel case, like `setName`. This requests that the module run the reducer. It returns a `Promise` which rejects with `SenderError` if the reducer fails, so `await` it in a `try`/`catch` to handle errors.
- A callback registation method, whose name is prefixed with `on`, like `onSetName`. This registers a callback to run whenever we are notified that the reducer ran, including successfully committed runs and runs we requested which failed. This method returns a callback id, which can be passed to the callback remove method.
- A callback remove method, whose name is prefixed with `removeOn`, like `removeOnSetName`. This cancels a callback previously registered via the callback registration method.
+For example:
+
+```typescript
+try {
+ await conn.reducers.setName({ name: newName });
+} catch (err) {
+ if (err instanceof SenderError) {
+ console.log(`You made an error: ${err}`);
+ } else if (err instanceof InternalError) {
+ console.log(`The server had an error: ${err}`);
+ }
+}
+```
+
## React Integration
The SpacetimeDB TypeScript SDK includes React bindings under the `spacetimedb/react` subpath. These bindings provide a `SpacetimeDBProvider` component and hooks for easily integrating SpacetimeDB into React applications.
diff --git a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
index 30b02f10801..dc2f5109501 100644
--- a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
+++ b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
@@ -29,6 +29,10 @@ These access rules are expressed in SQL and evaluated automatically for queries
RLS is **experimental and unstable**. It must be explicitly enabled in your module, and the API may change in future releases.
+
+No explicit opt-in is required in TypeScript; the feature is still experimental and the API may change.
+
+
To enable RLS, include the following preprocessor directive at the top of your module files:
@@ -50,6 +54,17 @@ spacetimedb = { version = "...", features = ["unstable"] }
## How It Works
+
+RLS rules are expressed in SQL and declared as exported filters on your schema.
+
+```typescript
+/** A client can only see their account */
+export const accountFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT * FROM account WHERE account.identity = :sender'
+);
+```
+
+
RLS rules are expressed in SQL and declared as public static readonly fields of type `Filter`.
@@ -112,6 +127,21 @@ This means clients will be able to see to any row that matches at least one of t
#### Example
+
+
+```typescript
+/** A client can only see their account */
+export const accountFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT * FROM account WHERE account.identity = :sender'
+);
+
+/** An admin can see all accounts */
+export const accountFilterForAdmins = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT account.* FROM account JOIN admin WHERE admin.identity = :sender'
+);
+```
+
+
```cs
@@ -169,6 +199,30 @@ This ensures that data is never leaked through indirect access patterns.
#### Example
+
+
+```typescript
+/** A client can only see their account */
+export const accountFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT * FROM account WHERE account.identity = :sender'
+);
+
+/** An admin can see all accounts */
+export const accountFilterForAdmins = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT account.* FROM account JOIN admin WHERE admin.identity = :sender'
+);
+
+/**
+ * Explicitly filtering by client identity in this rule is not necessary,
+ * since the above RLS rules on `account` will be applied automatically.
+ * Hence a client can only see their player, but an admin can see all players.
+ */
+export const playerFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT p.* FROM account a JOIN player p ON a.id = p.id'
+);
+```
+
+
```cs
@@ -240,6 +294,20 @@ as this would result in infinite recursion.
#### Example: Self-Join
+
+
+```typescript
+/** A client can only see players on their same level */
+export const playerFilter = spacetimedb.clientVisibilityFilter.sql(`
+ SELECT q.*
+ FROM account a
+ JOIN player p ON a.id = p.id
+ JOIN player q on p.level = q.level
+ WHERE a.identity = :sender
+`);
+```
+
+
```cs
@@ -286,6 +354,21 @@ const PLAYER_FILTER: Filter = Filter::Sql("
This module will fail to publish because each rule depends on the other one.
+
+
+```typescript
+/** An account must have a corresponding player */
+export const accountFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT a.* FROM account a JOIN player p ON a.id = p.id WHERE a.identity = :sender'
+);
+
+/** A player must have a corresponding account */
+export const playerFilter = spacetimedb.clientVisibilityFilter.sql(
+ 'SELECT p.* FROM account a JOIN player p ON a.id = p.id WHERE a.identity = :sender'
+);
+```
+
+
```cs
diff --git a/docs/static/llms.md b/docs/static/llms.md
index 0534de9a80c..e7e7aa1a8e5 100644
--- a/docs/static/llms.md
+++ b/docs/static/llms.md
@@ -44,7 +44,8 @@ This section covers the fundamental concepts you need to understand to build app
- [Core Concepts](/docs/core-concepts): This section covers the fundamental concepts you need to understand to build applications with SpacetimeDB.
- [Authentication](/docs/core-concepts/authentication): SpacetimeDB modules are exposed to the open internet and anyone can connect to
-- [Auth0](/docs/core-concepts/authentication/Auth0): This guilde will walk you through integrating Auth0 authentication with your SpacetimeDB application.
+- [Auth0](/docs/core-concepts/authentication/Auth0): This guide will walk you through integrating Auth0 authentication with your SpacetimeDB application.
+- [Better Auth](/docs/core-concepts/authentication/BetterAuth): Better Auth is a TypeScript authentication
- [Clerk](/docs/core-concepts/authentication/Clerk): This guide will walk you through integrating Clerk authentication with your SpacetimeDB React application. You will configure a Clerk application, obtain a JWT from Clerk, and pass it to your SpacetimeDB connection as the authentication token.
- [Overview](/docs/core-concepts/authentication/spacetimeauth/): SpacetimeAuth is currently in beta, some features may not be available yet or
- [Configuring your project](/docs/core-concepts/authentication/spacetimeauth/configuring-a-project): SpacetimeAuth is currently in beta, some features may not be available yet or may change in the future. You might encounter bugs or issues while using the service. Please report any problems you encounter to help us improve SpacetimeAuth.
@@ -119,6 +120,7 @@ A module is a collection of functions and schema definitions, which can be writt
- [React Quickstart](/docs/quickstarts/react): Get a SpacetimeDB React app running in under 5 minutes.
- [Remix Quickstart](/docs/quickstarts/remix): Get a SpacetimeDB Remix app running in under 5 minutes.
- [Rust Quickstart](/docs/quickstarts/rust): Get a SpacetimeDB Rust app running in under 5 minutes.
+- [SolidJS Quickstart](/docs/quickstarts/solid): Get a SpacetimeDB SolidJS app running in under 5 minutes.
- [Svelte Quickstart](/docs/quickstarts/svelte): Get a SpacetimeDB Svelte app running in under 5 minutes.
- [TanStack Start Quickstart](/docs/quickstarts/tanstack): Get a SpacetimeDB app with TanStack Start running in under 5 minutes.
- [TypeScript Quickstart](/docs/quickstarts/typescript): Get a SpacetimeDB TypeScript app running in under 5 minutes.
diff --git a/skills/csharp-client/SKILL.md b/skills/csharp-client/SKILL.md
index f70f9506aab..a2d653d572f 100644
--- a/skills/csharp-client/SKILL.md
+++ b/skills/csharp-client/SKILL.md
@@ -15,6 +15,8 @@ metadata:
Install: `dotnet add package SpacetimeDB.ClientSDK`
+Generated bindings convert snake_case names to PascalCase, including row fields: a server column `trip_id` is `TripId` on client rows.
+
## Connection
```csharp
@@ -28,7 +30,7 @@ var conn = DbConnection.Builder()
.OnConnect((conn, identity, token) =>
{
Console.WriteLine($"Connected as: {identity}");
- // Save token for reconnection
+ // Save token for reconnection. Dev only: a lost token is unrecoverable; use OIDC in production.
File.WriteAllText("auth_token.txt", token);
conn.SubscriptionBuilder()
@@ -137,6 +139,8 @@ conn.Reducers.MovePlayer(10.0f, 20.0f);
conn.Reducers.SendMessage("Hello!");
```
+Reducer calls return `void`; observe failures via the reducer callbacks below (`Status.Failed`).
+
## Reducer Callbacks
```csharp
diff --git a/skills/typescript-client/SKILL.md b/skills/typescript-client/SKILL.md
index 22c2c3d9ed6..a37be3fa06f 100644
--- a/skills/typescript-client/SKILL.md
+++ b/skills/typescript-client/SKILL.md
@@ -13,6 +13,8 @@ metadata:
# SpacetimeDB TypeScript Client
+Generated bindings convert snake_case names to camelCase, including row fields: a server column `trip_id` is `tripId` on client rows.
+
## React: main.tsx
```typescript
@@ -28,7 +30,7 @@ function Root() {
DbConnection.builder()
.withUri(SPACETIMEDB_URI)
.withDatabaseName(MODULE_NAME)
- .withToken(localStorage.getItem('auth_token') || undefined),
+ .withToken(localStorage.getItem('auth_token') || undefined), // Dev only: a lost token is unrecoverable. Use an OIDC provider token in production.
[]
);
return (
@@ -78,8 +80,8 @@ function App() {
}
);
- // Call reducers with object syntax
- conn?.reducers.addRecord({ data });
+ // Call reducers with object syntax. Returns Promise; rejects with SenderError on failure.
+ conn?.reducers.addRecord({ data }).catch(console.error);
// Compare identities
const isMe = row.owner.toHexString() === myIdentity?.toHexString();
diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md
index e71e17a1290..90581084880 100644
--- a/skills/typescript-server/SKILL.md
+++ b/skills/typescript-server/SKILL.md
@@ -99,6 +99,8 @@ export const createEntity = spacetimedb.reducer(
export const doReset = spacetimedb.reducer((ctx) => { ... });
```
+Reducer args accept any column type, including arrays of custom types: `{ splits: t.array(Split) }`. Do not pass JSON strings for structured data.
+
## DB Operations
```typescript
@@ -143,6 +145,8 @@ const bytes: Uint8Array = ctx.random.fill(new Uint8Array(16));
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
```
+Do not construct `Identity` values from strings (e.g. `'hex' as Identity`): serialization fails and kills the module. Identities come from `ctx.sender` or `t.identity()` columns.
+
## Scheduled Tables
```typescript
@@ -202,7 +206,7 @@ export const myProfile = spacetimedb.view(
## Complete Example
```typescript
-import { schema, table, t } from 'spacetimedb/server';
+import { schema, table, t, SenderError } from 'spacetimedb/server';
const entity = table(
{ name: 'entity', public: true },
@@ -242,7 +246,7 @@ export const onDisconnect = spacetimedb.clientDisconnected((ctx) => {
export const createEntity = spacetimedb.reducer(
{ name: t.string() },
(ctx, { name }) => {
- if (ctx.db.entity.identity.find(ctx.sender)) throw new Error('already exists');
+ if (ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('already exists');
ctx.db.entity.insert({ identity: ctx.sender, name, active: true });
}
);
@@ -250,7 +254,7 @@ export const createEntity = spacetimedb.reducer(
export const addRecord = spacetimedb.reducer(
{ value: t.u32() },
(ctx, { value }) => {
- if (!ctx.db.entity.identity.find(ctx.sender)) throw new Error('not found');
+ if (!ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('not found');
ctx.db.record.insert({ id: 0n, owner: ctx.sender, value });
}
);
diff --git a/skills/unity/SKILL.md b/skills/unity/SKILL.md
index 0f18c22d03a..9e1d39a62b9 100644
--- a/skills/unity/SKILL.md
+++ b/skills/unity/SKILL.md
@@ -218,5 +218,5 @@ The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP bu
- Check that `link.xml` preserves SpacetimeDB types if you use assembly stripping
### Token Persistence
-Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback.
+Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. A lost token is unrecoverable and means a new identity, so this is for development; use an OIDC provider token in production.
diff --git a/skills/unreal/SKILL.md b/skills/unreal/SKILL.md
index c0c630db0bc..eb0bb381e59 100644
--- a/skills/unreal/SKILL.md
+++ b/skills/unreal/SKILL.md
@@ -369,7 +369,7 @@ FSpacetimeDBConnectionId CId = Context.GetConnectionId();
## Token Persistence
-Use the built-in `UCredentials` helper to save and load tokens:
+Use the built-in `UCredentials` helper to save and load tokens. A lost token is unrecoverable and means a new identity, so this is for development; use an OIDC provider token in production.
```cpp
UCredentials::Init(TEXT(".spacetime_token"));
From c6c70fa52e9837ff2ae3d3a6a156b36b46a02279 Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Wed, 8 Jul 2026 15:43:04 -0400
Subject: [PATCH 2/8] updates
---
.../00300-resources/00100-how-to/00400-row-level-security.md | 2 +-
skills/csharp-client/SKILL.md | 2 +-
skills/typescript-client/SKILL.md | 4 ++--
skills/unity/SKILL.md | 2 +-
skills/unreal/SKILL.md | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
index dc2f5109501..7327d99e97c 100644
--- a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
+++ b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md
@@ -351,7 +351,7 @@ const PLAYER_FILTER: Filter = Filter::Sql("
#### Example: Recursive Rules
-This module will fail to publish because each rule depends on the other one.
+This module will publish, but because each rule depends on the other one, client queries and subscriptions on these tables will fail with a cyclic dependency error.
diff --git a/skills/csharp-client/SKILL.md b/skills/csharp-client/SKILL.md
index a2d653d572f..b1e7a0bc3a0 100644
--- a/skills/csharp-client/SKILL.md
+++ b/skills/csharp-client/SKILL.md
@@ -30,7 +30,7 @@ var conn = DbConnection.Builder()
.OnConnect((conn, identity, token) =>
{
Console.WriteLine($"Connected as: {identity}");
- // Save token for reconnection. Dev only: a lost token is unrecoverable; use OIDC in production.
+ // Save token for reconnection
File.WriteAllText("auth_token.txt", token);
conn.SubscriptionBuilder()
diff --git a/skills/typescript-client/SKILL.md b/skills/typescript-client/SKILL.md
index a37be3fa06f..6847629edf5 100644
--- a/skills/typescript-client/SKILL.md
+++ b/skills/typescript-client/SKILL.md
@@ -30,7 +30,7 @@ function Root() {
DbConnection.builder()
.withUri(SPACETIMEDB_URI)
.withDatabaseName(MODULE_NAME)
- .withToken(localStorage.getItem('auth_token') || undefined), // Dev only: a lost token is unrecoverable. Use an OIDC provider token in production.
+ .withToken(localStorage.getItem('auth_token') || undefined),
[]
);
return (
@@ -80,7 +80,7 @@ function App() {
}
);
- // Call reducers with object syntax. Returns Promise; rejects with SenderError on failure.
+ // Call reducers with object syntax
conn?.reducers.addRecord({ data }).catch(console.error);
// Compare identities
diff --git a/skills/unity/SKILL.md b/skills/unity/SKILL.md
index 9e1d39a62b9..71ba9f2edbf 100644
--- a/skills/unity/SKILL.md
+++ b/skills/unity/SKILL.md
@@ -218,5 +218,5 @@ The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP bu
- Check that `link.xml` preserves SpacetimeDB types if you use assembly stripping
### Token Persistence
-Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. A lost token is unrecoverable and means a new identity, so this is for development; use an OIDC provider token in production.
+Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. Lost tokens are unrecoverable; use an OIDC provider token in production.
diff --git a/skills/unreal/SKILL.md b/skills/unreal/SKILL.md
index eb0bb381e59..a42e636a825 100644
--- a/skills/unreal/SKILL.md
+++ b/skills/unreal/SKILL.md
@@ -369,7 +369,7 @@ FSpacetimeDBConnectionId CId = Context.GetConnectionId();
## Token Persistence
-Use the built-in `UCredentials` helper to save and load tokens. A lost token is unrecoverable and means a new identity, so this is for development; use an OIDC provider token in production.
+Use the built-in `UCredentials` helper to save and load tokens. Lost tokens are unrecoverable; use an OIDC provider token in production.
```cpp
UCredentials::Init(TEXT(".spacetime_token"));
From 45f773f3b353545d1c540c02eec581cc11eb7847 Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Wed, 8 Jul 2026 15:56:30 -0400
Subject: [PATCH 3/8] token warnings
---
docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md | 2 +-
docs/docs/00100-intro/00200-quickstarts/00180-browser.md | 2 +-
docs/docs/00100-intro/00300-tutorials/00100-chat-app.md | 2 +-
skills/unity/SKILL.md | 2 +-
skills/unreal/SKILL.md | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
index 22081d55523..5706659258a 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
@@ -171,7 +171,7 @@ export default defineEventHandler(async () => {
The root `app.vue` wraps your app in a `SpacetimeDBProvider` that manages the WebSocket connection. The provider is wrapped in `ClientOnly` so it only runs in the browser, while SSR uses the server API route for initial data.
- The token saved to `localStorage` is tied to this browser and cannot be recovered if lost, so it is recommended for development only. For production, use an external [OIDC provider](../../00200-core-concepts/00500-authentication.md).
+ When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
```vue
diff --git a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
index c2c2c165a9b..052d3dd47fb 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
@@ -58,7 +58,7 @@ npm run build
:::
:::warning
- The token saved to `localStorage` is tied to this browser and cannot be recovered if lost, so it is recommended for development only. For production, use an external OIDC provider. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+ When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
:::
diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
index 44163d0e403..c82c951ee84 100644
--- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
+++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
@@ -1445,7 +1445,7 @@ Here we are configuring our SpacetimeDB connection by specifying the server URI,
We are also using `localStorage` to store our SpacetimeDB credentials. This way, we can reconnect to SpacetimeDB with the same `Identity` and token if we refresh the page. The first time we connect, we won't have any credentials stored, so we pass `undefined` to the `withToken` method. This will cause SpacetimeDB to generate new credentials for us.
:::warning
-Identities issued this way are tied to a single token. If that token is lost, there is no way to recover or re-authenticate as that identity, so this approach is recommended for **development only**. For production applications, use an external OIDC provider. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
:::
If you chose a different name for your database, replace `quickstart-chat` with that name, or republish your module as `quickstart-chat`.
diff --git a/skills/unity/SKILL.md b/skills/unity/SKILL.md
index 71ba9f2edbf..596a3a2278e 100644
--- a/skills/unity/SKILL.md
+++ b/skills/unity/SKILL.md
@@ -218,5 +218,5 @@ The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP bu
- Check that `link.xml` preserves SpacetimeDB types if you use assembly stripping
### Token Persistence
-Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. Lost tokens are unrecoverable; use an OIDC provider token in production.
+Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. A self-issued identity can't be recovered if its token is lost, so use an external OIDC provider in production for stable identities.
diff --git a/skills/unreal/SKILL.md b/skills/unreal/SKILL.md
index a42e636a825..28d93f132f8 100644
--- a/skills/unreal/SKILL.md
+++ b/skills/unreal/SKILL.md
@@ -369,7 +369,7 @@ FSpacetimeDBConnectionId CId = Context.GetConnectionId();
## Token Persistence
-Use the built-in `UCredentials` helper to save and load tokens. Lost tokens are unrecoverable; use an OIDC provider token in production.
+Use the built-in `UCredentials` helper to save and load tokens. A self-issued identity can't be recovered if its token is lost, so use an external OIDC provider in production for stable identities.
```cpp
UCredentials::Init(TEXT(".spacetime_token"));
From ec84cb6b3319fb00394057cbdcbb9028b85adf50 Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Thu, 9 Jul 2026 09:33:07 -0400
Subject: [PATCH 4/8] updates
---
docs/docs/00200-core-concepts/00300-tables.md | 17 +--
skills/csharp-server/SKILL.md | 117 ++++++------------
skills/typescript-server/SKILL.md | 100 ++++++---------
3 files changed, 85 insertions(+), 149 deletions(-)
diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md
index a0b0bb43d95..4a2250c6f51 100644
--- a/docs/docs/00200-core-concepts/00300-tables.md
+++ b/docs/docs/00200-core-concepts/00300-tables.md
@@ -220,7 +220,7 @@ The table `name` in your schema is used **verbatim** in SQL queries and subscrip
-The accessor name is converted from snake_case to camelCase:
+The accessor is the key passed to `schema({...})`, verbatim. By convention the key matches the table `name`:
```typescript
// Table definition
@@ -229,15 +229,18 @@ const player_scores = table(
{ /* columns */ }
);
-// Accessor uses camelCase
-ctx.db.playerScores.insert({ /* ... */ });
+const spacetimedb = schema({ player_scores });
+
+// Accessor is the schema key, verbatim
+ctx.db.player_scores.insert({ /* ... */ });
```
| Table Name | Accessor |
+| Schema Key | Accessor |
|------------|----------|
-| `'user'` | `ctx.db.user` |
-| `'player_scores'` | `ctx.db.playerScores` |
-| `'game_session'` | `ctx.db.gameSession` |
+| `user` | `ctx.db.user` |
+| `player_scores` | `ctx.db.player_scores` |
+| `game_session` | `ctx.db.game_session` |
@@ -319,7 +322,7 @@ Use idiomatic naming conventions for each language:
| Language | Convention | Example Table | Example Accessor |
|----------|------------|---------------|------------------|
-| **TypeScript** | snake_case | `'player_score'` | `ctx.db.playerScore` |
+| **TypeScript** | snake_case | `'player_score'` | `ctx.db.player_score` |
| **C#** | PascalCase | `Accessor = "PlayerScore"` | `ctx.Db.PlayerScore` |
| **Rust** | lower_snake_case | `name = player_score` | `ctx.db.player_score()` |
| **C++** | lower_snake_case | `player_score` | `ctx.db[player_score]` |
diff --git a/skills/csharp-server/SKILL.md b/skills/csharp-server/SKILL.md
index 05f353d5802..07759283716 100644
--- a/skills/csharp-server/SKILL.md
+++ b/skills/csharp-server/SKILL.md
@@ -13,28 +13,36 @@ metadata:
# SpacetimeDB C# SDK Reference
-## Imports
-
-```csharp
-using SpacetimeDB;
-```
-
## Module Structure
-All tables, types, and reducers go inside a static partial class:
+All tables, types, and reducers go inside one `public static partial class Module`; the only import is `using SpacetimeDB;`:
```csharp
using SpacetimeDB;
public static partial class Module
{
- // Tables, types, and reducers here
+ [SpacetimeDB.Table(Accessor = "ScoreRecord", Public = true)]
+ public partial struct ScoreRecord
+ {
+ [PrimaryKey]
+ [AutoInc]
+ public ulong Id;
+ public Identity Owner;
+ public uint Value;
+ }
+
+ [SpacetimeDB.Reducer]
+ public static void AddRecord(ReducerContext ctx, uint value)
+ {
+ ctx.Db.ScoreRecord.Insert(new ScoreRecord { Id = 0, Owner = ctx.Sender, Value = value });
+ }
}
```
## Tables
-`[SpacetimeDB.Table(...)]` on a `public partial struct`. `Accessor` should be PascalCase:
+`[SpacetimeDB.Table(...)]` on a `public partial struct` inside the `Module` class. `Accessor` should be PascalCase:
```csharp
[SpacetimeDB.Table(Accessor = "Entity", Public = true)]
@@ -51,7 +59,7 @@ public partial struct Entity
Options: `Accessor = "PascalCase"` (recommended), `Public = true`, `Scheduled = nameof(ReducerFn)`, `ScheduledAt = nameof(field)`, `Event = true`
-`ctx.Db` accessors use the `Accessor` name: `ctx.Db.Entity`, `ctx.Db.Record`.
+`ctx.Db` accessors use the `Accessor` name: `ctx.Db.Entity`, `ctx.Db.ScoreRecord`.
## Column Types
@@ -73,6 +81,8 @@ Options: `Accessor = "PascalCase"` (recommended), `Public = true`, `Scheduled =
## Column Attributes
+The complete set of column attributes:
+
```csharp
[PrimaryKey] // primary key
[AutoInc] // auto-increment (use 0 as placeholder on insert)
@@ -82,7 +92,7 @@ Options: `Accessor = "PascalCase"` (recommended), `Public = true`, `Scheduled =
## Indexes
-Prefer `[SpacetimeDB.Index.BTree]` inline for single-column. Multi-column uses struct-level:
+Write the index attribute fully qualified: `[SpacetimeDB.Index.BTree]`. Prefer inline for single-column; multi-column uses struct-level:
```csharp
// Inline (preferred for single-column):
@@ -185,6 +195,8 @@ timestamp.MicrosecondsSinceUnixEpoch / 1000
## Scheduled Tables
+Declare the scheduled table and its reducer in the same `Module` class so `nameof(...)` resolves:
+
```csharp
[SpacetimeDB.Table(
Accessor = "TickTimer",
@@ -222,10 +234,23 @@ public enum Status { Online, Away, Offline }
[SpacetimeDB.Type]
public partial struct Point { public float X; public float Y; }
+```
+
+Tagged enums (discriminated unions): a `partial record` with empty body and no constructor parameters. Payloads are `[Type] partial struct`s:
+
+```csharp
+[SpacetimeDB.Type]
+public partial struct Circle { public int Radius; }
-// Tagged enum (discriminated union):
[SpacetimeDB.Type]
-public partial record MyUnion : SpacetimeDB.TaggedEnum<(string Text, int Number)>;
+public partial struct Rectangle { public int Width; public int Height; }
+
+[SpacetimeDB.Type]
+public partial record Shape : SpacetimeDB.TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
+
+// Construct variants via the generated nested constructors:
+var a = new Shape.Circle(new Circle { Radius = 10 });
+var b = new Shape.Rectangle(new Rectangle { Width = 4, Height = 6 });
```
## Optional Fields
@@ -241,69 +266,3 @@ public partial struct Player
public uint? HighScore;
}
```
-
-## Complete Example
-
-```csharp
-using SpacetimeDB;
-
-[SpacetimeDB.Table(Accessor = "Entity", Public = true)]
-public partial struct Entity
-{
- [PrimaryKey]
- public Identity Identity;
- public string Name;
- public bool Active;
-}
-
-[SpacetimeDB.Table(Accessor = "Record", Public = true)]
-public partial struct Record
-{
- [PrimaryKey]
- [AutoInc]
- public ulong Id;
- public Identity Owner;
- public uint Value;
- public Timestamp CreatedAt;
-}
-
-public static partial class Module
-{
- [SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
- public static void OnConnect(ReducerContext ctx)
- {
- var existing = ctx.Db.Entity.Identity.Find(ctx.Sender);
- if (existing is not null)
- ctx.Db.Entity.Identity.Update(existing.Value with { Active = true });
- }
-
- [SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
- public static void OnDisconnect(ReducerContext ctx)
- {
- var existing = ctx.Db.Entity.Identity.Find(ctx.Sender);
- if (existing is not null)
- ctx.Db.Entity.Identity.Update(existing.Value with { Active = false });
- }
-
- [SpacetimeDB.Reducer]
- public static void CreateEntity(ReducerContext ctx, string name)
- {
- if (ctx.Db.Entity.Identity.Find(ctx.Sender) is not null)
- throw new Exception("already exists");
- ctx.Db.Entity.Insert(new Entity { Identity = ctx.Sender, Name = name, Active = true });
- }
-
- [SpacetimeDB.Reducer]
- public static void AddRecord(ReducerContext ctx, uint value)
- {
- if (ctx.Db.Entity.Identity.Find(ctx.Sender) is null)
- throw new Exception("not found");
- ctx.Db.Record.Insert(new Record {
- Id = 0,
- Owner = ctx.Sender,
- Value = value,
- CreatedAt = ctx.Timestamp,
- });
- }
-}
-```
diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md
index 90581084880..e62793a7086 100644
--- a/skills/typescript-server/SKILL.md
+++ b/skills/typescript-server/SKILL.md
@@ -13,8 +13,37 @@ metadata:
# SpacetimeDB TypeScript SDK Reference
+## Module Structure
+
+Tables are built with `table()`, bound with `schema()`, and exported as default. Reducers and lifecycle hooks are `export const`:
+
+```typescript
+import { schema, table, t } from 'spacetimedb/server';
+
+const score_record = table(
+ { name: 'score_record', public: true },
+ {
+ id: t.u64().primaryKey().autoInc(),
+ owner: t.identity(),
+ value: t.u32(),
+ }
+);
+
+const spacetimedb = schema({ score_record });
+export default spacetimedb;
+
+export const addRecord = spacetimedb.reducer(
+ { value: t.u32() },
+ (ctx, { value }) => {
+ ctx.db.score_record.insert({ id: 0n, owner: ctx.sender, value });
+ }
+);
+```
+
## Imports
+`spacetimedb/server` is the only import path for server modules:
+
```typescript
import { schema, table, t } from 'spacetimedb/server';
import { SenderError } from 'spacetimedb/server';
@@ -38,10 +67,12 @@ const entity = table(
Options: `name` (snake_case, recommended), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`
-`ctx.db` accessors are the camelCase form of the table's `name` field.
+`ctx.db` accessors are the keys passed to `schema({...})`, verbatim: `schema({ score_record })` → `ctx.db.score_record`. Use snake_case keys matching the table `name`. Client codegen converts case; server `ctx.db` does not.
## Column Types
+Every column is a `t` builder value:
+
| Builder | JS type | Notes |
|---------|---------|-------|
| `t.u64()` | bigint | Use `0n` literals |
@@ -56,7 +87,7 @@ Options: `name` (snake_case, recommended), `public: true`, `event: true`, `sched
| `t.timeDuration()` | TimeDuration | |
| `t.scheduleAt()` | ScheduleAt | |
-Modifiers: `.primaryKey()`, `.autoInc()`, `.unique()`, `.index('btree')`
+Modifiers (complete set): `.primaryKey()`, `.autoInc()`, `.unique()`, `.index('btree')`
Optional columns: `nickname: t.option(t.string())`
@@ -79,13 +110,13 @@ When you frequently look up rows by multiple columns, prefer a multi-column inde
## Schema Export
```typescript
-const spacetimedb = schema({ entity, record }); // ONE object, not spread args
+const spacetimedb = schema({ entity, score_record }); // ONE object, not spread args
export default spacetimedb;
```
## Reducers
-Export name becomes the reducer name:
+Reducers are created with `spacetimedb.reducer(...)`; the export name becomes the reducer name:
```typescript
export const createEntity = spacetimedb.reducer(
@@ -150,7 +181,7 @@ Do not construct `Identity` values from strings (e.g. `'hex' as Identity`): seri
## Scheduled Tables
```typescript
-const tickTimer = table({
+const tick_timer = table({
name: 'tick_timer',
scheduled: (): any => tick, // (): any => breaks circular dep
}, {
@@ -159,7 +190,7 @@ const tickTimer = table({
});
export const tick = spacetimedb.reducer(
- { timer: tickTimer.rowType },
+ { timer: tick_timer.rowType },
(ctx, { timer }) => { /* timer row auto-deleted after this runs */ }
);
@@ -202,60 +233,3 @@ export const myProfile = spacetimedb.view(
(ctx) => ctx.db.entity.identity.find(ctx.sender) ?? undefined
);
```
-
-## Complete Example
-
-```typescript
-import { schema, table, t, SenderError } from 'spacetimedb/server';
-
-const entity = table(
- { name: 'entity', public: true },
- {
- identity: t.identity().primaryKey(),
- name: t.string(),
- active: t.bool(),
- }
-);
-
-const record = table(
- {
- name: 'record',
- public: true,
- indexes: [{ accessor: 'by_owner', algorithm: 'btree', columns: ['owner'] }],
- },
- {
- id: t.u64().primaryKey().autoInc(),
- owner: t.identity(),
- value: t.u32(),
- }
-);
-
-const spacetimedb = schema({ entity, record });
-export default spacetimedb;
-
-export const onConnect = spacetimedb.clientConnected((ctx) => {
- const existing = ctx.db.entity.identity.find(ctx.sender);
- if (existing) ctx.db.entity.identity.update({ ...existing, active: true });
-});
-
-export const onDisconnect = spacetimedb.clientDisconnected((ctx) => {
- const existing = ctx.db.entity.identity.find(ctx.sender);
- if (existing) ctx.db.entity.identity.update({ ...existing, active: false });
-});
-
-export const createEntity = spacetimedb.reducer(
- { name: t.string() },
- (ctx, { name }) => {
- if (ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('already exists');
- ctx.db.entity.insert({ identity: ctx.sender, name, active: true });
- }
-);
-
-export const addRecord = spacetimedb.reducer(
- { value: t.u32() },
- (ctx, { value }) => {
- if (!ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('not found');
- ctx.db.record.insert({ id: 0n, owner: ctx.sender, value });
- }
-);
-```
From f74d14b826049b043d276e9feb2ae9dcbfd0cf2b Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Thu, 9 Jul 2026 09:39:02 -0400
Subject: [PATCH 5/8] updates
---
skills/csharp-server/SKILL.md | 22 +++++-----------------
skills/typescript-server/SKILL.md | 11 ++---------
2 files changed, 7 insertions(+), 26 deletions(-)
diff --git a/skills/csharp-server/SKILL.md b/skills/csharp-server/SKILL.md
index 07759283716..a0caf46aa32 100644
--- a/skills/csharp-server/SKILL.md
+++ b/skills/csharp-server/SKILL.md
@@ -15,7 +15,7 @@ metadata:
## Module Structure
-All tables, types, and reducers go inside one `public static partial class Module`; the only import is `using SpacetimeDB;`:
+Reducers are static methods in a `static partial class`; tables are `public partial struct`s. This reference keeps everything in one `public static partial class Module`, which needs only `using SpacetimeDB;`:
```csharp
using SpacetimeDB;
@@ -42,7 +42,7 @@ public static partial class Module
## Tables
-`[SpacetimeDB.Table(...)]` on a `public partial struct` inside the `Module` class. `Accessor` should be PascalCase:
+`[SpacetimeDB.Table(...)]` on a `public partial struct`. `Accessor` should be PascalCase:
```csharp
[SpacetimeDB.Table(Accessor = "Entity", Public = true)]
@@ -79,6 +79,8 @@ Options: `Accessor = "PascalCase"` (recommended), `Public = true`, `Scheduled =
| `TimeDuration` | duration in microseconds |
| `Uuid` | UUID |
+Optional columns: nullable types (`string? Nickname`, `uint? HighScore`)
+
## Column Attributes
The complete set of column attributes:
@@ -106,7 +108,7 @@ public ulong AuthorId;
public partial struct Membership { public ulong GroupId; public Identity UserId; ... }
```
-When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results.
+Prefer a multi-column index over filtering by one column and looping.
## Reducers
@@ -252,17 +254,3 @@ public partial record Shape : SpacetimeDB.TaggedEnum<(Circle Circle, Rectangle R
var a = new Shape.Circle(new Circle { Radius = 10 });
var b = new Shape.Rectangle(new Rectangle { Width = 4, Height = 6 });
```
-
-## Optional Fields
-
-```csharp
-[SpacetimeDB.Table(Accessor = "Player")]
-public partial struct Player
-{
- [PrimaryKey, AutoInc]
- public ulong Id;
- public string Name;
- public string? Nickname;
- public uint? HighScore;
-}
-```
diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md
index e62793a7086..b89960fb891 100644
--- a/skills/typescript-server/SKILL.md
+++ b/skills/typescript-server/SKILL.md
@@ -29,7 +29,7 @@ const score_record = table(
}
);
-const spacetimedb = schema({ score_record });
+const spacetimedb = schema({ score_record }); // ONE object, not spread args
export default spacetimedb;
export const addRecord = spacetimedb.reducer(
@@ -105,14 +105,7 @@ indexes: [{ accessor: 'by_group_user', algorithm: 'btree', columns: ['groupId',
// Access: ctx.db.membership.by_group_user.filter([groupId, userId]);
```
-When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results. Multi-column filter takes an array matching the index column order. You can omit trailing columns to do a prefix scan.
-
-## Schema Export
-
-```typescript
-const spacetimedb = schema({ entity, score_record }); // ONE object, not spread args
-export default spacetimedb;
-```
+Prefer a multi-column index over filtering by one column and looping. Filter takes an array in index column order; a prefix scan passes the leading value bare: `filter(groupId)`.
## Reducers
From 25f57a4da229d0e846bb6ccbca73a9e5298bf83f Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Thu, 9 Jul 2026 09:39:05 -0400
Subject: [PATCH 6/8] Update 00300-tables.md
---
docs/docs/00200-core-concepts/00300-tables.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md
index 4a2250c6f51..5a1ece9bcdb 100644
--- a/docs/docs/00200-core-concepts/00300-tables.md
+++ b/docs/docs/00200-core-concepts/00300-tables.md
@@ -235,7 +235,6 @@ const spacetimedb = schema({ player_scores });
ctx.db.player_scores.insert({ /* ... */ });
```
-| Table Name | Accessor |
| Schema Key | Accessor |
|------------|----------|
| `user` | `ctx.db.user` |
From 4df2f1b59b147e3154a726693ca24a812de0cde8 Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Thu, 9 Jul 2026 12:20:44 -0400
Subject: [PATCH 7/8] updates
---
skills/concepts/SKILL.md | 2 +-
skills/csharp-server/SKILL.md | 2 +-
skills/rust-server/SKILL.md | 4 ++--
skills/typescript-server/SKILL.md | 12 ++++++------
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/skills/concepts/SKILL.md b/skills/concepts/SKILL.md
index ac9e91b371a..8ea6e8f9cfb 100644
--- a/skills/concepts/SKILL.md
+++ b/skills/concepts/SKILL.md
@@ -20,7 +20,7 @@ SpacetimeDB is a relational database that is also a server. It lets you upload a
## Critical Rules
1. **Reducers are transactional.** They do not return data to callers. Use subscriptions to read data.
-2. **Reducers must be deterministic.** Do not use filesystem, network, external clocks, or external random sources in reducers. Use `ReducerContext` for SpacetimeDB-provided timestamp and deterministic random values.
+2. **Reducers must be deterministic.** Do not use filesystem, network, external clocks, or external random sources in reducers. Use the reducer context (`ctx`) for SpacetimeDB-provided timestamp and deterministic random values.
3. **Read data via tables/subscriptions**, not reducer return values. Clients get data through subscribed queries.
4. **Auto-increment IDs are not sequential.** Gaps are normal, do not use for ordering. Use timestamps or explicit sequence columns.
5. **`ctx.sender` is the authenticated principal.** Never trust identity passed as arguments.
diff --git a/skills/csharp-server/SKILL.md b/skills/csharp-server/SKILL.md
index a0caf46aa32..f19f647fb08 100644
--- a/skills/csharp-server/SKILL.md
+++ b/skills/csharp-server/SKILL.md
@@ -174,7 +174,7 @@ public static Entity? MyProfile(ViewContext ctx)
## Reducer Context API
-`ReducerContext` is the single source of sender identity, deterministic time, and deterministic randomness inside a reducer. Always go through `ctx` for these. Standard library clocks and random sources are not available in modules.
+`ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules.
```csharp
// Auth: ctx.Sender is the caller's Identity
diff --git a/skills/rust-server/SKILL.md b/skills/rust-server/SKILL.md
index 99d2e87d23c..f83a4387b1c 100644
--- a/skills/rust-server/SKILL.md
+++ b/skills/rust-server/SKILL.md
@@ -120,7 +120,7 @@ ctx.db.entity().identity().find(ctx.sender()); // Find by un
ctx.db.item().author_id().filter(author_id); // Filter by index → iterator
ctx.db.entity().iter(); // All rows → iterator
ctx.db.entity().count(); // Count rows
-ctx.db.entity().id().update(Entity { ..existing, name: new_name }); // Update (spread + override)
+ctx.db.entity().id().update(Entity { name: new_name, ..existing }); // Update (override + spread)
ctx.db.entity().id().delete(entity_id); // Delete by PK
ctx.db.entity().name().delete("Alice"); // Delete by indexed column
```
@@ -164,7 +164,7 @@ fn my_profile(ctx: &ViewContext) -> Option {
## Reducer Context API
-`ReducerContext` is the single source of sender identity, deterministic time, and deterministic randomness inside a reducer. Always go through `ctx` for these. Standard library clocks and random sources are not available in modules.
+`ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules.
```rust
// Auth: ctx.sender() is the caller's Identity
diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md
index b89960fb891..2e4db3ba262 100644
--- a/skills/typescript-server/SKILL.md
+++ b/skills/typescript-server/SKILL.md
@@ -128,13 +128,13 @@ Reducer args accept any column type, including arrays of custom types: `{ splits
## DB Operations
```typescript
-ctx.db.entity.insert({ id: 0n, name: 'Sample' }); // Insert (0n for autoInc)
-ctx.db.entity.id.find(entityId); // Find by PK → row | null
+ctx.db.score_record.insert({ id: 0n, owner: ctx.sender, value: 1 }); // Insert (0n for autoInc)
+ctx.db.score_record.id.find(recordId); // Find by PK → row | null
ctx.db.entity.identity.find(ctx.sender); // Find by unique column
-[...ctx.db.item.authorId.filter(authorId)]; // Filter → spread to Array
+[...ctx.db.post.authorId.filter(authorId)]; // Filter → spread to Array
[...ctx.db.entity.iter()]; // All rows → Array
-ctx.db.entity.id.update({ ...existing, name: newName }); // Update (spread + override)
-ctx.db.entity.id.delete(entityId); // Delete by PK
+ctx.db.score_record.id.update({ ...existing, value: 2 }); // Update (spread + override)
+ctx.db.score_record.id.delete(recordId); // Delete by PK
```
Note: `iter()` and `filter()` return iterators. Spread to Array for `.sort()`, `.filter()`, `.map()`.
@@ -151,7 +151,7 @@ export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... });
## Reducer Context API
-`ReducerContext` is the single source of sender identity, deterministic time, and deterministic randomness inside a reducer. Always go through `ctx` for these. Standard library clocks and random sources are not available in modules.
+`ctx` is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules. In helpers, type it as `ReducerCtx>`.
```typescript
// Auth: ctx.sender is the caller's Identity
From 057eab7c4f5e0b4c0f53a48e4baa3576d60d4fad Mon Sep 17 00:00:00 2001
From: bradleyshep <148254416+bradleyshep@users.noreply.github.com>
Date: Thu, 9 Jul 2026 13:31:44 -0400
Subject: [PATCH 8/8] refinements
---
docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md | 2 +-
docs/docs/00100-intro/00200-quickstarts/00180-browser.md | 2 +-
docs/docs/00100-intro/00300-tutorials/00100-chat-app.md | 2 +-
skills/unity/SKILL.md | 2 +-
skills/unreal/SKILL.md | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
index 5706659258a..3bc36455dd0 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00155-nuxt.md
@@ -171,7 +171,7 @@ export default defineEventHandler(async () => {
The root `app.vue` wraps your app in a `SpacetimeDBProvider` that manages the WebSocket connection. The provider is wrapped in `ClientOnly` so it only runs in the browser, while SSR uses the server API route for initial data.
- When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+ With no token, SpacetimeDB issues a server-issued identity and a non-expiring token; persist it and pass it back on reconnect to keep the same identity. A lost token can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle. See [Authentication](../../00200-core-concepts/00500-authentication.md).
```vue
diff --git a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
index 052d3dd47fb..87c70dbff02 100644
--- a/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
+++ b/docs/docs/00100-intro/00200-quickstarts/00180-browser.md
@@ -58,7 +58,7 @@ npm run build
:::
:::warning
- When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+ With no token, SpacetimeDB issues a server-issued identity and a non-expiring token; persist it and pass it back on reconnect to keep the same identity. A lost token can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle. See [Authentication](../../00200-core-concepts/00500-authentication.md).
:::
diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
index c82c951ee84..e4d16284e75 100644
--- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
+++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
@@ -1445,7 +1445,7 @@ Here we are configuring our SpacetimeDB connection by specifying the server URI,
We are also using `localStorage` to store our SpacetimeDB credentials. This way, we can reconnect to SpacetimeDB with the same `Identity` and token if we refresh the page. The first time we connect, we won't have any credentials stored, so we pass `undefined` to the `withToken` method. This will cause SpacetimeDB to generate new credentials for us.
:::warning
-When no token is provided, SpacetimeDB issues a new identity that can't be recovered if the stored token is later lost. This is fine for development; in production, use an external OIDC provider so each user keeps a stable identity. See [Authentication](../../00200-core-concepts/00500-authentication.md).
+With no token, SpacetimeDB issues a server-issued identity and a non-expiring token; persist it and pass it back on reconnect to keep the same identity. A lost token can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle. See [Authentication](../../00200-core-concepts/00500-authentication.md).
:::
If you chose a different name for your database, replace `quickstart-chat` with that name, or republish your module as `quickstart-chat`.
diff --git a/skills/unity/SKILL.md b/skills/unity/SKILL.md
index 596a3a2278e..61d523f1e97 100644
--- a/skills/unity/SKILL.md
+++ b/skills/unity/SKILL.md
@@ -218,5 +218,5 @@ The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP bu
- Check that `link.xml` preserves SpacetimeDB types if you use assembly stripping
### Token Persistence
-Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback. A self-issued identity can't be recovered if its token is lost, so use an external OIDC provider in production for stable identities.
+Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. Persisting the server-issued token and passing it back on reconnect keeps the same identity; without a saved token the server issues a new identity in the `OnConnect` callback. This token does not expire and a lost one can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle.
diff --git a/skills/unreal/SKILL.md b/skills/unreal/SKILL.md
index 28d93f132f8..1995bed1e5e 100644
--- a/skills/unreal/SKILL.md
+++ b/skills/unreal/SKILL.md
@@ -369,7 +369,7 @@ FSpacetimeDBConnectionId CId = Context.GetConnectionId();
## Token Persistence
-Use the built-in `UCredentials` helper to save and load tokens. A self-issued identity can't be recovered if its token is lost, so use an external OIDC provider in production for stable identities.
+Use the built-in `UCredentials` helper to save and load tokens. Passing the saved server-issued token back on reconnect keeps the same identity. This token does not expire and a lost one can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle.
```cpp
UCredentials::Init(TEXT(".spacetime_token"));