From 513a809d508ffcc7272a73de49059203c6ada6a9 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 28 Feb 2026 15:29:17 -0500 Subject: [PATCH 1/4] Add spacetime.json documentation page Reference docs for the spacetime.json config file (proposal 0032), covering config structure, field reference, generate configuration, children/inheritance, dev configuration, database selection, flag overrides, environments, config discovery, and editor support. --- .../00300-spacetime-json.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 docs/docs/00300-resources/01000-reference/00100-cli-reference/00300-spacetime-json.md diff --git a/docs/docs/00300-resources/01000-reference/00100-cli-reference/00300-spacetime-json.md b/docs/docs/00300-resources/01000-reference/00100-cli-reference/00300-spacetime-json.md new file mode 100644 index 00000000000..091d6fd5064 --- /dev/null +++ b/docs/docs/00300-resources/01000-reference/00100-cli-reference/00300-spacetime-json.md @@ -0,0 +1,338 @@ +--- +title: spacetime.json +slug: /cli-reference/spacetime-json +--- + +# `spacetime.json` Configuration File + +The `spacetime.json` file defines project-level configuration for the SpacetimeDB CLI. It eliminates repetitive CLI flags and enables multi-target workflows such as publishing multiple databases or generating bindings for multiple languages from a single project. + +Commands that read `spacetime.json` include [`spacetime publish`](/cli-reference#spacetime-publish), [`spacetime generate`](/cli-reference#spacetime-generate), and [`spacetime dev`](/cli-reference#spacetime-dev). + +## Config structure + +The config is database-centric. The top-level object represents the root database, which is always required. The root database may have `children` that define additional database targets, each inheriting the root's settings by default. + +```json +{ + "database": "my-game", + "module-path": "./server", + "dev": { "run": "pnpm dev" }, + "generate": [ + { "language": "typescript", "out-dir": "./client/src/bindings" } + ] +} +``` + +## Fields reference + +These fields can appear at any level (root or child): + +| Field | Type | Inherited | Description | +|-------|------|-----------|-------------| +| `database` | string | No | Database name or identity (required) | +| `module-path` | string | Yes\* | Path to module source directory | +| `bin-path` | string | Yes\* | Path to precompiled WASM binary | +| `js-path` | string | Yes\* | Path to bundled JavaScript file | +| `server` | string | Yes | Server nickname, domain, or URL | +| `build-options` | string | Yes | Options passed to the build command | +| `break-clients` | boolean | Yes | Allow breaking changes | +| `num-replicas` | number | Yes | Number of database replicas | +| `anonymous` | boolean | Yes | Use anonymous identity | +| `organization` | string | Yes | Organization name or identity | +| `generate` | array | No | Generate targets (see [Generate configuration](#generate-configuration)) | +| `children` | array | No | Child database entities (see [Children and inheritance](#children-and-inheritance)) | +| `dev` | object | No | Dev server configuration, root-level only (see [`spacetime dev` configuration](#spacetime-dev-configuration)) | + +\* `module-path`, `bin-path`, and `js-path` are mutually exclusive. If a child specifies any one of these, the other two are not inherited from the parent. See [Source conflict rule](#source-conflict-rule). + +## Generate configuration + +The `generate` key is an array of objects. Each object configures bindings generation for a specific language and output location. + +| Field | Type | Description | +|-------|------|-------------| +| `language` | string | Target language: `typescript`, `csharp`, `rust`, `unrealcpp` (required) | +| `out-dir` | string | Output directory for generated files | +| `namespace` | string | C# namespace (`csharp` only) | +| `unreal-module-name` | string | Unreal module name (`unrealcpp` only) | +| `uproject-dir` | string | Unreal project directory (`unrealcpp` only) | +| `include-private` | boolean | Include private tables in generated code | + +Generate entries use the `module-path` (or `bin-path`/`js-path`) from their parent entity to determine which module to build and generate from. + +When `spacetime generate` runs, it deduplicates by module path. If multiple databases share the same module and generate config (for example, via inheritance), bindings are generated once. + +### Example + +```json +{ + "database": "my-game", + "module-path": "./server", + "generate": [ + { "language": "typescript", "out-dir": "./web/src/bindings" }, + { + "language": "csharp", + "out-dir": "./unity/Assets/Bindings", + "namespace": "MyGame.Bindings" + } + ] +} +``` + +## Children and inheritance + +The `children` array defines additional database targets. Each child inherits most fields from the root by default. + +### What children inherit + +All fields listed in the [Fields reference](#fields-reference) with "Yes" in the Inherited column are inherited. A child can override any inherited field by specifying it explicitly. + +The following fields are never inherited: + +- `database`: each child must define its own. +- `generate`: tied to a specific module and output location. Inheriting generate targets is redundant when the child shares the parent's module (deduplication already handles it) and dangerous when the child uses a different module (two modules would write bindings into the same output directory). +- `children`: structural, not a database property. +- `dev`: root-level only. + +### Source conflict rule + +`module-path`, `bin-path`, and `js-path` are mutually exclusive module sources (mirroring the CLI's existing conflict group for `--module-path`, `--bin-path`, and `--js-path`). If a child specifies any one of these, the other two are not inherited from the parent. This prevents a child from accidentally inheriting a `bin-path` that points to a different module's precompiled binary. + +### Multi-database example + +```json +{ + "database": "region-us", + "module-path": "./region-module", + "server": "testnet", + "build-options": "--release", + "generate": [ + { "language": "typescript", "out-dir": "./client/src/bindings" } + ], + "children": [ + { "database": "region-eu" }, + { "database": "region-asia" } + ] +} +``` + +All three databases (`region-us`, `region-eu`, `region-asia`) share the same module, server, and build options via inheritance. Because all three databases use the same module and generate config, bindings are generated only once. + +### Different modules + +A child can override `module-path` to use a different module: + +```json +{ + "database": "my-game-global", + "module-path": "./global-module", + "server": "testnet", + "generate": [ + { "language": "typescript", "out-dir": "./web/src/global-bindings" }, + { "language": "csharp", "out-dir": "./unity/Assets/GlobalBindings" } + ], + "children": [ + { + "database": "my-game-region", + "module-path": "./region-module", + "generate": [ + { "language": "typescript", "out-dir": "./web/src/region-bindings" } + ] + } + ] +} +``` + +The child overrides `module-path` and `generate`, while inheriting `server` from the root. + +## `spacetime dev` configuration + +The `dev` key is a root-level-only setting that specifies the client development server command: + +```json +{ + "dev": { "run": "pnpm dev" } +} +``` + +The `--run` CLI flag overrides `dev.run`. + +### Behavior + +When running `spacetime dev`: + +1. Build and publish all databases defined in the config. +2. Generate bindings for all databases. +3. Run the client dev server specified by `dev.run`. +4. Watch for changes and repeat steps 1-3. + +The `--server` flag overrides the server for all databases. The `--skip-publish` flag skips step 1, and `--skip-generate` skips step 2. + +### Config auto-generation + +If no config file exists, `spacetime dev` generates a `spacetime.dev.json` with a minimal config. The command prompts for the database name (pre-filled with the directory name as the default) and infers the client language and output directory from the project structure. Values that the CLI already defaults (such as `module-path` to `./spacetimedb`) are omitted. + +### Safety prompt + +`spacetime dev` tracks which config file the publish targets were resolved from. If the publish configuration comes from `spacetime.json` or `spacetime.local.json` (rather than a dev-specific file like `spacetime.dev.json` or `spacetime.dev.local.json`), the command prompts for confirmation before publishing. This prevents accidentally publishing to a shared or production server during development. + +## Database selection + +When `spacetime.json` exists, the database name positional argument selects which databases to operate on. Glob patterns are supported. + +```bash +# Operate on all databases +spacetime publish + +# Operate on a specific database +spacetime publish region-us + +# Operate on databases matching a pattern +spacetime publish "region-*" +``` + +When no database name is provided, all databases are selected. When a filter matches no databases, the CLI reports an error: + +``` +$ spacetime publish typo-name +Error: No database 'typo-name' found in spacetime.json. +Use --no-config to ignore the config file. +``` + +## Flag overrides + +All CLI flags besides the database selector act as overrides. They are classified as global, per-database, or per-generate-entry: + +### Global overrides + +These apply to all selected databases: + +- `--server`: target server +- `--build-options`: build flags +- `--break-clients`: allow breaking changes +- `--delete-data`: clear database data +- `--yes` / `--force`: skip confirmation prompts + +### Per-database overrides + +These produce an error if multiple databases are selected: + +- `--module-path`: module source path +- `--bin-path`: precompiled WASM path +- `--js-path`: JS bundle path +- `--num-replicas`: replica count + +### Per-generate-entry overrides + +These produce an error if the selected database has multiple generate entries: + +- `--lang`: target language +- `--out-dir`: output directory +- `--namespace`: C# namespace +- `--unreal-module-name`: Unreal module name +- `--uproject-dir`: Unreal project directory + +## `--no-config` + +The `--no-config` flag causes the CLI to ignore `spacetime.json` entirely, behaving as if no config file exists. This is useful for one-off operations or scripting. + +```bash +spacetime publish my-db --module-path ./module --no-config +``` + +## `--env` and environments + +Config files support environment and local overrides via a naming convention: + +| File | Purpose | Checked in to git? | +|------|---------|---------------------| +| `spacetime.json` | Project defaults | Yes | +| `spacetime.{env}.json` | Environment-specific config | Yes | +| `spacetime.local.json` | User-specific overrides | No | +| `spacetime.{env}.local.json` | User + environment-specific overrides | No | + +The *env* placeholder is set via the `--env` CLI flag (for example, `--env dev`). The `spacetime dev` command implicitly uses `--env dev`. + +When `--env` is not specified, `spacetime publish` and `spacetime generate` load only the base config files (`spacetime.json` and `spacetime.local.json`). + +### Precedence + +From highest to lowest priority: + +1. CLI flags (highest) +2. `spacetime.{env}.local.json` +3. `spacetime.local.json` +4. `spacetime.{env}.json` +5. `spacetime.json` +6. Built-in defaults (lowest) + +CLI flags only override config values when explicitly provided by the user. Default flag values do not override config file values. + +### Override behavior + +Higher-priority files replace whole keys, not merge them. For example: + +```json +// spacetime.json (checked in, shared project defaults) +{ + "database": "my-game", + "module-path": "./server", + "server": "maincloud", + "generate": [ + { "language": "typescript", "out-dir": "./client/src/bindings" }, + { "language": "csharp", "out-dir": "./unity/Assets/Bindings" } + ] +} +``` + +```json +// spacetime.dev.json (checked in, development environment) +{ + "server": "local", + "database": "my-game-dev" +} +``` + +The result when running with `--env dev` (or via `spacetime dev`, which implies `--env dev`): + +```json +{ + "database": "my-game-dev", + "module-path": "./server", + "server": "local", + "generate": [ + { "language": "typescript", "out-dir": "./client/src/bindings" }, + { "language": "csharp", "out-dir": "./unity/Assets/Bindings" } + ] +} +``` + +The `database` and `server` fields come from `spacetime.dev.json`. The `module-path` and `generate` fields are inherited from the base `spacetime.json`. + +A developer can further override with a local file: + +```json +// spacetime.dev.local.json (NOT checked in, personal overrides) +{ + "database": "my-game-dev-tyler" +} +``` + +This gives each developer their own database name for development without conflicts, while sharing the rest of the dev configuration. + +## Config file discovery + +The CLI searches for `spacetime.json` starting from the current directory and walking up the directory tree until it finds one or reaches the filesystem root. All paths in the config are relative to the directory containing `spacetime.json`. + +## Editor support + +The config file uses JSON5 syntax, which supports comments and trailing commas. The file uses a `.json` extension for broad editor compatibility. + +Editors that enforce strict JSON on `.json` files will flag comments as errors. In VSCode, this can be resolved by adding to your settings: + +```json +{ + "files.associations": { "spacetime.json": "jsonc" } +} +``` From 1a1fe7859e43ce1372139b6b868b51fa44aec065 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 28 Feb 2026 18:01:27 -0500 Subject: [PATCH 2/4] Fix spacetime.json docs and code to match proposal Docs fixes: - Use game-world region names instead of cloud regions (region-us etc.) - Replace "testnet" server with "maincloud" - Fix config precedence order to match Vite's convention - Fix spacetime dev step ordering (build, generate, publish) - Fix config auto-generation description (spacetime.json + spacetime.local.json) - Fix safety prompt description (only spacetime.json triggers it) - Move --build-options from global to per-database overrides Code fixes: - Stop inheriting generate from parent to children, preventing different modules from silently overwriting each other's bindings - Implement source conflict rule: if a child specifies any of module-path/bin-path/js-path, the others are not inherited - Mark --num-replicas as module_specific (per-database override) - Update tests to match new generate inheritance behavior --- crates/cli/src/spacetime_config.rs | 76 ++++++++++--------- crates/cli/src/subcommands/dev.rs | 1 + crates/cli/src/subcommands/publish.rs | 2 +- .../00300-spacetime-json.md | 43 ++++++----- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/crates/cli/src/spacetime_config.rs b/crates/cli/src/spacetime_config.rs index 9c49ed5f22c..d093312e535 100644 --- a/crates/cli/src/spacetime_config.rs +++ b/crates/cli/src/spacetime_config.rs @@ -164,46 +164,56 @@ pub struct LoadedConfig { impl SpacetimeConfig { /// Collect all database targets with parent→child inheritance. - /// Children inherit unset `additional_fields` and `generate` from their parent. - /// `dev` and `children` are NOT propagated to child targets. + /// Children inherit unset `additional_fields` from their parent. + /// `dev`, `generate`, and `children` are NOT propagated to child targets. /// Returns `Vec` with fully resolved fields. pub fn collect_all_targets_with_inheritance(&self) -> Vec { - self.collect_targets_inner(None, None) + self.collect_targets_inner(None) } fn collect_targets_inner( &self, parent_fields: Option<&HashMap>, - parent_generate: Option<&Vec>>, ) -> Vec { + // module-path, bin-path, and js-path are mutually exclusive module sources. + // If a child specifies any one, the other two are not inherited from the parent. + const MODULE_SOURCE_KEYS: &[&str] = &["module-path", "bin-path", "js-path"]; + let child_specifies_source = MODULE_SOURCE_KEYS + .iter() + .any(|k| self.additional_fields.contains_key(*k)); + // Build this node's fields by inheriting from parent let mut fields = self.additional_fields.clone(); if let Some(parent) = parent_fields { for (key, value) in parent { - if !fields.contains_key(key) { - fields.insert(key.clone(), value.clone()); + if fields.contains_key(key) { + continue; + } + // If the child specifies any module source, skip inheriting the others + if child_specifies_source && MODULE_SOURCE_KEYS.contains(&key.as_str()) { + continue; } + fields.insert(key.clone(), value.clone()); } } - // Generate: child's generate replaces parent's; if absent, inherit parent's - let effective_generate = if self.generate.is_some() { - self.generate.clone() - } else { - parent_generate.cloned() - }; + // Generate is never inherited. It is tied to a specific module and output location: + // inheriting is redundant when the child shares the parent's module (deduplication + // handles it) and dangerous when the child uses a different module (two modules + // would write bindings to the same output directory). + let effective_generate = self.generate.clone(); let target = FlatTarget { fields: fields.clone(), source_config: self.source_config.clone(), - generate: effective_generate.clone(), + generate: effective_generate, }; let mut result = vec![target]; if let Some(children) = &self.children { for child in children { - let child_targets = child.collect_targets_inner(Some(&fields), effective_generate.as_ref()); + let child_targets = child.collect_targets_inner(Some(&fields)); result.extend(child_targets); } } @@ -2600,8 +2610,8 @@ mod tests { } #[test] - fn test_generate_inheritance_from_parent() { - // Children inherit generate from parent if they don't define their own + fn test_generate_not_inherited_from_parent() { + // Generate is never inherited. Children must define their own. let json = r#"{ "database": "parent-db", "server": "local", @@ -2632,15 +2642,10 @@ mod tests { Some("typescript") ); - // Child 1 inherits parent's generate - let child1_gen = targets[1].generate.as_ref().unwrap(); - assert_eq!(child1_gen.len(), 1); - assert_eq!( - child1_gen[0].get("language").and_then(|v| v.as_str()), - Some("typescript") - ); + // Child 1 does not inherit parent's generate + assert!(targets[1].generate.is_none()); - // Child 2 overrides with its own generate + // Child 2 has its own generate let child2_gen = targets[2].generate.as_ref().unwrap(); assert_eq!(child2_gen.len(), 1); assert_eq!(child2_gen[0].get("language").and_then(|v| v.as_str()), Some("csharp")); @@ -3084,9 +3089,10 @@ mod tests { } #[test] - fn test_generate_dedup_with_inherited_generate() { - // Two sibling databases sharing parent's generate + same module path - // should deduplicate to a single generate entry + fn test_generate_not_inherited_for_children_sharing_module() { + // Even when children share the parent's module-path, generate is not inherited. + // Deduplication in generate.rs handles the common case; inheritance would be + // dangerous when a child overrides module-path. let json = r#"{ "module-path": "./server", "generate": [ @@ -3101,20 +3107,22 @@ mod tests { let config: SpacetimeConfig = json5::from_str(json).unwrap(); let targets = config.collect_all_targets_with_inheritance(); - // All 3 targets (parent + 2 children) share the same module-path and generate assert_eq!(targets.len(), 3); + + // Parent has generate + assert!(targets[0].generate.is_some()); + + // Children do not inherit generate + assert!(targets[1].generate.is_none()); + assert!(targets[2].generate.is_none()); + + // All share the same module-path via field inheritance for target in &targets { assert_eq!( target.fields.get("module-path").and_then(|v| v.as_str()), Some("./server") ); - let gen = target.generate.as_ref().unwrap(); - assert_eq!(gen.len(), 1); - assert_eq!(gen[0].get("language").and_then(|v| v.as_str()), Some("typescript")); } - - // All have the same (module-path, generate) so dedup should reduce to 1 - // (this is verified in generate.rs tests, but we confirm the data here) } #[test] diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index bfa96f17990..6086c428e89 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -680,6 +680,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E } // Safety prompt: warn if any selected database target is defined in spacetime.json. + // spacetime.local.json is gitignored and personal, so it's fine for dev use. if let Some(ref lc) = loaded_config { let database_sources = resolve_database_sources(&lc.config); let databases_from_main_config: Vec = db_names_for_logging diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index 578aee87bd9..3ecffa0da5c 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -28,7 +28,7 @@ pub fn build_publish_schema(command: &clap::Command) -> Result Date: Mon, 2 Mar 2026 12:07:49 -0500 Subject: [PATCH 3/4] cargo fmt --- crates/cli/src/spacetime_config.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/cli/src/spacetime_config.rs b/crates/cli/src/spacetime_config.rs index d093312e535..8fa8efdf4b6 100644 --- a/crates/cli/src/spacetime_config.rs +++ b/crates/cli/src/spacetime_config.rs @@ -171,10 +171,7 @@ impl SpacetimeConfig { self.collect_targets_inner(None) } - fn collect_targets_inner( - &self, - parent_fields: Option<&HashMap>, - ) -> Vec { + fn collect_targets_inner(&self, parent_fields: Option<&HashMap>) -> Vec { // module-path, bin-path, and js-path are mutually exclusive module sources. // If a child specifies any one, the other two are not inherited from the parent. const MODULE_SOURCE_KEYS: &[&str] = &["module-path", "bin-path", "js-path"]; From 5a2cdb1ca6d5002dd7ed27affdc721af909f5498 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Thu, 5 Mar 2026 16:17:43 -0500 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Phoebe Goldman Signed-off-by: Tyler Cloutier --- crates/cli/src/spacetime_config.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/spacetime_config.rs b/crates/cli/src/spacetime_config.rs index 8fa8efdf4b6..40db920c372 100644 --- a/crates/cli/src/spacetime_config.rs +++ b/crates/cli/src/spacetime_config.rs @@ -3086,10 +3086,11 @@ mod tests { } #[test] + /// Even when children share the parent's module-path, generate is not inherited. + /// + /// Deduplication in generate.rs handles the common case; inheritance would be + /// dangerous when a child overrides module-path. fn test_generate_not_inherited_for_children_sharing_module() { - // Even when children share the parent's module-path, generate is not inherited. - // Deduplication in generate.rs handles the common case; inheritance would be - // dangerous when a child overrides module-path. let json = r#"{ "module-path": "./server", "generate": [