Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9222259
Add database environment storage, SQL, and module bindings
cloutiertyler Sep 8, 2026
4acc884
Extend V10 visibility and expose trusted invocation authority
cloutiertyler Sep 8, 2026
8ef580d
Preserve qualified reducer names and complete stacked binding tests
cloutiertyler Sep 8, 2026
203a022
Adapt invocation authority host fixture to current engine construction
cloutiertyler Sep 8, 2026
55ef08d
Retain current compiler feature flags in visibility codegen regression
cloutiertyler Sep 8, 2026
4613001
Implement typed publish-only database environments
cloutiertyler Sep 8, 2026
05a8053
Clarify module library terminology in environment docs
cloutiertyler Sep 8, 2026
523f045
Merge declared ENV into V10 ABI extensions
cloutiertyler Sep 8, 2026
648ae25
Address environment API and CLI review feedback
cloutiertyler Sep 9, 2026
a36a974
Merge reviewed ENV updates into V10 ABI extensions
cloutiertyler Sep 9, 2026
7d26b8d
Fix ENV CI coordination and restore C++ logging names
cloutiertyler Sep 9, 2026
60c17d0
Fix environment CI isolation and update handler diagnostics
cloutiertyler Sep 9, 2026
0293b62
Document direct HTTP environment publishing and retain query diagnostics
cloutiertyler Sep 9, 2026
03cb28c
Add typed Rust environment enums and show values in env list
cloutiertyler Sep 9, 2026
2bd321b
Fix ENV test CLI inspection and isolated root discovery
cloutiertyler Sep 9, 2026
93f89e9
Add Rust module-test environment example and runtime coverage
cloutiertyler Sep 9, 2026
60992ad
Apply ENV system table and test naming review cleanup
cloutiertyler Sep 9, 2026
b491ae4
Simplify ENV schema declaration state and example naming
cloutiertyler Sep 9, 2026
b0c498b
Clarify standalone publication locking and ENV recovery invariants
cloutiertyler Sep 9, 2026
ce37d26
Merge ENV review fixes into V10 ABI extensions
cloutiertyler Sep 9, 2026
29b7780
Format C# ENV and invocation authority imports
cloutiertyler Sep 9, 2026
9847404
Regenerate CLI docs for private function visibility
cloutiertyler Sep 9, 2026
0b1abdb
Regenerate canonical C# environment metadata bindings
cloutiertyler Sep 9, 2026
5096852
Merge canonical environment bindings into V10 ABI stack
cloutiertyler Sep 9, 2026
d8379a6
Align ABI regression fixtures with V10 schema metadata
cloutiertyler Sep 9, 2026
a420f86
Fix C++ and C# procedure tests to request V10 schemas
cloutiertyler Sep 9, 2026
87169bd
Clarify environment usage and lower rejected SQL logging to debug
cloutiertyler Sep 10, 2026
291e02d
Avoid inherited member collisions in C# environment accessors
cloutiertyler Sep 10, 2026
81a4190
Separate generic view handling from ENV changes
cloutiertyler Sep 10, 2026
8340992
Merge reviewed ENV fixes into ABI prerequisites
cloutiertyler Sep 10, 2026
776689b
Check canonical procedure names in V10 schema HTTP test
cloutiertyler Sep 10, 2026
a7c7799
Add to C++ HandlerContext
JasonAtClockwork Sep 10, 2026
e98ef53
Preserve Windows inspector PATH and clarify secret visibility
cloutiertyler Sep 10, 2026
1bc09a3
Merge current master into environment implementation
cloutiertyler Sep 10, 2026
b7adb97
Update persistent environment publication and management
cloutiertyler Sep 12, 2026
f8684bb
Integrate persistent environment publishing into V10 ABI
cloutiertyler Sep 12, 2026
642a50b
Clarify stored environment keys and publish precedence
cloutiertyler Sep 12, 2026
964407b
Carry ENV documentation clarifications into ABI stack
cloutiertyler Sep 12, 2026
bb3036c
Merge branch 'master' into tyler/environment-variables
bfops Sep 12, 2026
a10bbd8
Remove superseded workflow coordinator changes
cloutiertyler Sep 12, 2026
48fc6dd
Merge commit 'a10bbd8d04f3a6b66a162657b333dea5ea51014d' into tyler/v1…
cloutiertyler Sep 12, 2026
ea49bda
Drop redundant pnpm script workaround after CI pinning fix
cloutiertyler Sep 13, 2026
8ac8cf7
Merge commit 'ea49bdadfd3eac14454ac0f40ac9cb0f887e53f6' into HEAD
cloutiertyler Sep 13, 2026
554e946
Merge master into V10 ABI extensions
cloutiertyler Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ members = [
"modules/perf-test",
"modules/module-test",
"modules/environment-test",
"modules/invocation-flags-test",
"templates/basic-rs/spacetimedb",
"templates/chat-console-rs/spacetimedb",
"modules/sdk-test",
Expand Down
24 changes: 24 additions & 0 deletions crates/bindings-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

The SpacetimeDB C++ Module Library provides a modern C++20 API for building SpacetimeDB modules that run inside the database as WebAssembly.

## Function visibility and invocation authentication

Apply `SPACETIMEDB_FUNCTION_VISIBILITY(name, Public)`, `Private`, or `Internal`
to a reducer or procedure after its definition:

```cpp
SPACETIMEDB_REDUCER(process_jobs, ReducerContext ctx) {
return Ok();
}
SPACETIMEDB_FUNCTION_VISIBILITY(process_jobs, Internal);
```

Omission means public for ordinary functions and private for scheduled functions.
An explicit choice is preserved when the function is scheduled. Lifecycle
reducers permit only omission or `Internal` and can only run for their host
lifecycle event. Internal functions require verified internal authority. Private
functions also admit the owner, and public functions admit any client.

`ctx.sender_auth().is_internal()` captures the host's invocation authority. It is
independent of connection and JWT presence, so an internal call can have a JWT.
JWT identity is the verified sender supplied by the host. Procedures preserve
this authentication in `with_tx` and `try_with_tx`. Newly compiled modules emit
schema V10 and advertise `hosted_auth_v1`, requiring a compatible host.

## Current State

This library provides a production-ready C++ bindings for SpacetimeDB with complete type system support:
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-cpp/include/spacetimedb/abi/FFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ using ::identity;
// ===== JWT =====
using ::get_jwt;
using ::env_get;
using ::get_call_auth_flags;

// ===== Procedure Transactions =====
using ::procedure_start_mut_tx;
Expand Down
5 changes: 5 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/abi/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

#define STDB_IMPORT_10_6(name) \
__attribute__((import_module("spacetime_10.6"), import_name(#name))) extern
#define STDB_IMPORT_10_7(name) \
__attribute__((import_module("spacetime_10.7"), import_name(#name))) extern

// Import opaque types into global namespace for C compatibility
using SpacetimeDB::Status;
Expand All @@ -64,6 +66,9 @@ extern "C" {

STDB_IMPORT_10_6(env_get)
Status env_get(const uint8_t* key, uint32_t key_len, BytesSource* out);
// Verified invocation authority. Bit 0 is INTERNAL; JWT presence is independent.
STDB_IMPORT_10_7(get_call_auth_flags)
uint32_t get_call_auth_flags();

// ===== Table and Index Management =====
STDB_IMPORT(table_id_from_name)
Expand Down
90 changes: 42 additions & 48 deletions crates/bindings-cpp/include/spacetimedb/auth_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,25 @@ struct ConnectionId;
class AuthCtx {
private:
bool is_internal_;
std::optional<Identity> verified_sender_;
mutable std::shared_ptr<std::optional<JwtClaims>> jwt_;
std::function<std::optional<JwtClaims>()> jwt_loader_;

// Private constructor used by factory methods
AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader);
AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader,
std::optional<Identity> verified_sender = std::nullopt);
static AuthCtx from_connection_with_flags(ConnectionId connection_id, Identity sender, uint32_t flags);

public:
/**
* @brief Creates an AuthCtx from an optional ConnectionId.
*
* If the connection_id is present, creates an AuthCtx that will load the JWT.
* If the connection_id is absent, creates an internal AuthCtx.
* Internal authority is captured from the host, independently of connection presence.
*
* @param connection_id Optional connection ID
* @param sender The identity of the caller (already derived from JWT claims by the host)
* @return An AuthCtx based on the connection_id
* @param sender The verified caller Identity supplied by the host
* @return An AuthCtx with captured invocation authority and lazy JWT loading
*/
static AuthCtx from_connection_id_opt(std::optional<ConnectionId> connection_id, Identity sender);

Expand All @@ -63,11 +66,10 @@ class AuthCtx {
* This is primarily used for testing purposes, allowing you to create
* an AuthCtx with specific JWT claims without needing a real connection.
*
* Note: The Identity must be computed by calling the host function,
* as we cannot compute Blake3 hashes in WASM.
* The Identity must be the verified sender supplied by the host.
*
* @param jwt_payload The raw JWT payload (JSON claims)
* @param identity The identity derived from the JWT's issuer and subject
* @param identity The verified sender Identity
* @return An AuthCtx with the provided JWT
*/
static AuthCtx from_jwt_payload(std::string jwt_payload, Identity identity);
Expand All @@ -76,11 +78,10 @@ class AuthCtx {
* @brief Creates an AuthCtx that reads the JWT for the given connection ID.
*
* The JWT will be lazily loaded from the host when first accessed.
* The identity parameter is the sender's identity, already derived from
* JWT claims by the host (using Blake3 hashing).
* The identity parameter is the verified sender supplied by the host.
*
* @param connection_id The connection ID to load the JWT for
* @param sender The identity of the caller (already derived from JWT claims by the host)
* @param sender The verified sender Identity supplied by the host
* @return An AuthCtx that will load the JWT on demand
*/
static AuthCtx from_connection_id(ConnectionId connection_id, Identity sender);
Expand All @@ -93,9 +94,9 @@ class AuthCtx {
bool is_internal() const { return is_internal_; }

/**
* @brief Checks if there is a JWT without loading it.
* @brief Checks if there is a JWT, loading it lazily if necessary.
*
* If is_internal() returns true, this will return false.
* Independent of is_internal(). Internal calls can also have a JWT.
*
* @return true if a JWT is available
*/
Expand All @@ -113,9 +114,8 @@ class AuthCtx {
/**
* @brief Gets the caller's identity.
*
* For internal calls, this returns the database's identity.
* For external calls, this returns the identity derived from the JWT
* (based on the issuer and subject claims).
* Returns the verified sender captured when constructing the context,
* independently of JWT presence or token claims.
*
* @return The caller's Identity
*/
Expand All @@ -126,16 +126,16 @@ class AuthCtx {
// INLINE IMPLEMENTATIONS
// ============================================================================

constexpr uint16_t ERROR_BUFFER_TOO_SMALL = 11;

inline AuthCtx::AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader)
: is_internal_(is_internal), jwt_loader_(std::move(loader)) {}
inline AuthCtx::AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader,
std::optional<Identity> verified_sender)
: is_internal_(is_internal), verified_sender_(std::move(verified_sender)), jwt_loader_(std::move(loader)) {}

inline AuthCtx AuthCtx::from_connection_id_opt(std::optional<ConnectionId> connection_id, Identity sender) {
const auto flags = FFI::get_call_auth_flags();
if (connection_id.has_value()) {
return from_connection_id(*connection_id, std::move(sender));
return from_connection_with_flags(*connection_id, std::move(sender), flags);
} else {
return internal();
return AuthCtx((flags & 1) != 0, []() -> std::optional<JwtClaims> { return std::nullopt; }, sender);
}
}

Expand All @@ -144,13 +144,17 @@ inline AuthCtx AuthCtx::internal() {
}

inline AuthCtx AuthCtx::from_jwt_payload(std::string jwt_payload, Identity identity) {
return AuthCtx(false, [payload = std::move(jwt_payload), id = std::move(identity)]() mutable -> std::optional<JwtClaims> {
return AuthCtx(false, [payload = std::move(jwt_payload), id = identity]() mutable -> std::optional<JwtClaims> {
return JwtClaims(std::move(payload), std::move(id));
});
}, identity);
}

inline AuthCtx AuthCtx::from_connection_id(ConnectionId connection_id, Identity sender) {
return AuthCtx(false, [connection_id, sender]() -> std::optional<JwtClaims> {
return from_connection_with_flags(connection_id, std::move(sender), FFI::get_call_auth_flags());
}

inline AuthCtx AuthCtx::from_connection_with_flags(ConnectionId connection_id, Identity sender, uint32_t flags) {
return AuthCtx((flags & 1) != 0, [connection_id, sender]() -> std::optional<JwtClaims> {
// Call the host FFI to get the JWT
BytesSource jwt_source;

Expand All @@ -169,35 +173,24 @@ inline AuthCtx AuthCtx::from_connection_id(ConnectionId connection_id, Identity
}

// Read the JWT payload from the BytesSource
std::vector<uint8_t> buffer;
buffer.resize(4096); // Start with 4KB buffer

size_t buffer_len = buffer.size();
int16_t result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);

while (result == ERROR_BUFFER_TOO_SMALL) {
buffer.resize(buffer.size() * 2);
buffer_len = buffer.size();
result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);
std::array<uint8_t, 4096> buffer;
std::string jwt_payload;
for (;;) {
size_t buffer_len = buffer.size();
const auto result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);
if (result != 0 && result != -1) return std::nullopt;
jwt_payload.append(reinterpret_cast<const char*>(buffer.data()), buffer_len);
// -1 is successful exhaustion and may include the final payload bytes.
if (result == -1) break;
if (buffer_len == 0) return std::nullopt;
}

if (result < 0) {
return std::nullopt;
}

// Convert bytes to string
std::string jwt_payload(buffer.begin(), buffer.begin() + buffer_len);

// Use the provided sender identity (already computed by host from JWT claims)
if (jwt_payload.empty()) return std::nullopt;
// Token claims cannot override the verified sender, including hosted tokens.
return JwtClaims(std::move(jwt_payload), sender);
});
}, sender);
}

inline bool AuthCtx::has_jwt() const {
if (is_internal_) {
return false;
}

// Load the JWT if not already loaded, then check if it has a value
// This ensures has_jwt() and get_jwt() are consistent
return get_jwt().has_value();
Expand All @@ -211,6 +204,7 @@ inline const std::optional<JwtClaims>& AuthCtx::get_jwt() const {
}

inline Identity AuthCtx::get_caller_identity() const {
if (verified_sender_.has_value()) return *verified_sender_;
if (is_internal_) {
// Return database identity for internal calls
std::array<uint8_t, 32> identity_bytes;
Expand Down
6 changes: 6 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/function_visibility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

namespace SpacetimeDB {
// Omission preserves the host default: Public ordinarily, Private when scheduled.
enum class FunctionVisibility { Public, Private, Internal };
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ namespace SpacetimeDB::Internal {
enum class FunctionVisibility : uint8_t {
Private = 0,
ClientCallable = 1,
Internal = 2,
ExplicitClientCallable = 3,
};
} // namespace SpacetimeDB::Internal
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@

namespace SpacetimeDB::Internal {

SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>, std::vector<SpacetimeDB::Internal::RawSubmoduleV10>, std::vector<SpacetimeDB::Internal::EnvironmentDeclaration>)
SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>, std::vector<SpacetimeDB::Internal::RawSubmoduleV10>, std::vector<SpacetimeDB::Internal::EnvironmentDeclaration>, std::vector<std::string>)
} // namespace SpacetimeDB::Internal
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <cstdio>
#include "../bsatn/bsatn.h"
#include "../database.h"
#include "../function_visibility.h"
#include "autogen/CaseConversionPolicy.g.h"
#include "autogen/ExplicitNameEntry.g.h"
#include "autogen/NameMapping.g.h"
Expand Down Expand Up @@ -49,6 +50,7 @@ void fail_reducer(std::string message);

namespace Internal {

// Builds the V10 module definition with explicit function visibility.
class V10Builder {
public:
V10Builder() = default;
Expand Down Expand Up @@ -437,7 +439,7 @@ class V10Builder {
RawReducerDefV10 reducer_def{
reducer_name,
ProductType{},
FunctionVisibility::Private,
FunctionVisibility::Internal,
MakeUnitAlgebraicType(),
MakeStringAlgebraicType(),
};
Expand Down Expand Up @@ -646,6 +648,7 @@ class V10Builder {

void RegisterExplicitTableName(const std::string& source_name, const std::string& canonical_name);
void RegisterExplicitFunctionName(const std::string& source_name, const std::string& canonical_name);
void SetFunctionVisibility(const std::string& source_name, ::SpacetimeDB::FunctionVisibility visibility);
void RegisterExplicitIndexName(const std::string& source_name, const std::string& canonical_name);

RawModuleDefV10 BuildModuleDef() const;
Expand Down
12 changes: 5 additions & 7 deletions crates/bindings-cpp/include/spacetimedb/jwt_claims.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace SpacetimeDB {
* This class provides lazy parsing of JWT claims, parsing specific fields
* on demand. It follows the same pattern as the Rust and C# implementations.
*
* The Identity is provided in the constructor because computing it requires
* Blake3 hashing, which is done on the host side.
* The Identity is the verified sender supplied by the host. Token claims
* cannot override it, including for hosted container credentials.
*/
class JwtClaims {
private:
Expand All @@ -36,11 +36,10 @@ class JwtClaims {
/**
* @brief Constructs a JwtClaims from a JWT payload and its associated Identity.
*
* The Identity must be provided because computing it requires Blake3 hashing,
* which is performed on the host side.
* The Identity must be the verified sender supplied by the host.
*
* @param jwt_payload The raw JWT payload (JSON claims)
* @param identity The identity derived from the JWT's issuer and subject
* @param identity The verified sender Identity
*/
JwtClaims(std::string jwt_payload, Identity identity);

Expand Down Expand Up @@ -71,8 +70,7 @@ class JwtClaims {
/**
* @brief Returns the identity for these credentials.
*
* The identity is based on the 'iss' and 'sub' claims and is computed
* using Blake3 hashing on the host side.
* This is the verified sender supplied by the host, independently of claims.
*
* @return The identity
*/
Expand Down
10 changes: 9 additions & 1 deletion crates/bindings-cpp/include/spacetimedb/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,15 @@ inline std::vector<std::string> parseParameterNames(const std::string& param_lis
// VISIBILITY FILTER MACRO
// =============================================================================

// Apply to a registered reducer or procedure. Runs after function registration;
// lifecycle reducers only accept Internal. Scheduling preserves this choice.
#define SPACETIMEDB_FUNCTION_VISIBILITY(function_name, visibility) \
extern "C" __attribute__((export_name("__preinit__40_visibility_" #function_name))) \
void CONCAT(__spacetimedb_function_visibility_, function_name)() { \
::SpacetimeDB::Internal::getV10Builder().SetFunctionVisibility( \
#function_name, ::SpacetimeDB::FunctionVisibility::visibility); \
}

/**
* @brief Set module case conversion policy using a fixed preinit registration symbol.
*
Expand Down Expand Up @@ -917,4 +926,3 @@ inline std::vector<std::string> parseParameterNames(const std::string& param_lis


#endif // SPACETIMEDB_MACROS_H

Loading
Loading