YAML Source is the Network Storage authoring standard.
Use the s&box Library Manager install for auto-updates. GitHub is best for agents, source review, contributions, and manual installs.

Collections

Collections store your game data. Each collection has a type and schema that defines what data it accepts. Runtime access is controlled through endpoints, queri...

# Collections

Collections store your game data. Each collection has a type and schema that defines what data it accepts. Runtime access is controlled through endpoints, queries, or trusted secret-key tooling.

For direct dedicated-server/backend reads and writes, see [Collections HTTP API](/wiki/network-storage-v3/collections-http-api).

Use **YAML Source** for new collection definitions. For the complete YAML syntax, including collection fields, schema options, constants, tables, tests, libraries, and compiler metadata, see [Source Authoring](/wiki/network-storage-v3/source-authoring).

## YAML Source and JSON Schema

Collections are stored and validated as structured JSON on the server, even when you author them as YAML. YAML Source is the editable source document; the backend compiles it into a canonical collection object that the runtime and dashboard forms use.

For a YAML Source collection, this field:

```yml
definition:
schema:
gold:
type: "number"
default: 0
```

compiles to the same schema data:

```yml
schema:
gold:
type: number
default: 0
```

The dashboard may show both views:

| View | What it shows |
|------|---------------|
| YAML Source | The original source document, including `sourceVersion`, `kind`, `id`, `name`, and `definition` |
| JSON schema | The compiled `definition.schema` object used for validation |

So seeing a JSON schema in the collection editor does not mean the project is using an old `.json` collection file. It is the canonical form generated from the YAML `definition.schema`. For source-managed projects, prefer editing the YAML Source locally or in the source editor, then push with the Sync Tool. Editing only the schema form changes the canonical schema and can diverge from the saved YAML source unless the source is regenerated.

## Collection Types

### per-player

Data is keyed by Steam ID. Each player has their own document in the collection.

```
player_data/
76561198000001/ { xp: 1500, gold: 3200, playerName: "Hero" }
76561198000002/ { xp: 800, gold: 1100, playerName: "Mage" }
76561198000003/ { xp: 25000, gold: 15000, playerName: "Veteran" }
```

Use per-player collections for character stats, inventory, settings, quest progress, and anything tied to a single player.

Note that there is no `level` field stored. Level is computed from XP using Game Values: `floor(xp / xp_per_level)`. See the [Game Values](/wiki/network-storage-v3/game-values) docs for details.

### global

A single shared document (or set of documents) accessible to all players. Not keyed by Steam ID.

```
leaderboard/
weekly_top/ { entries: [...] }
alltime_top/ { entries: [...] }

server_config/
settings/ { pvpEnabled: true, maxPlayers: 64 }
```

Use global collections for leaderboards, server config, shared world state, and data that all players read from.

## Access Model

Collections are endpoint/query controlled for game clients. Direct collection API calls require a secret key with collection execute permission and are intended for dedicated-server/backoffice tooling.

### endpoint controlled

Game clients read and write collection data through endpoints and queries. Public-key direct collection API calls are rejected with `403 ENDPOINT_ONLY`. Server-side logic controls exactly how data gets handled.

```yml
ok: false
error: ENDPOINT_ONLY
message: This collection can only be accessed through endpoints.
```

Use endpoint-controlled collections for economy, progression, inventory, casual stats, settings, and anything where you want to prevent exploiting or currency manipulation. The game client must go through your endpoint/query pipelines, which enforce validation, rate limits, and workflows before any data is saved.

For maximum security, run a **dedicated server** and keep secret keys on the server only. Game clients should call endpoints and queries; direct collection APIs require a secret key with collection execute permission.

### Choosing an access model

| Use Case | Recommended Mode | Why |
|----------|-----------------|-----|
| Player economy, currency, progression | Endpoint | Prevents clients from writing arbitrary values. Server decides rewards. |
| Inventory, unlocks, quest state | Endpoint | Endpoints validate ownership, costs, and prerequisites before writing. |
| Mini-game scores, casual stats | Endpoint | Expose a small endpoint and keep writes controlled. |
| Cosmetic settings, preferences | Endpoint | Expose a read/write endpoint for preferences. |
| Ranked / competitive data | Endpoint + Dedicated Server | Zero exploit potential. Only your server calls trusted endpoints or secret-key tooling. |

## Creating a Collection

From your project page, click **New Collection**. The collection editor opens in **YAML Source** mode for new collections.

### Sample: Per-Player RPG Collection

Create a file named `player_data.collection.yml`:

```yml
sourceVersion: "1"
kind: collection
name: player_data
description: Player progression and inventory
collectionType: per-steamid
accessMode: endpoint
maxRecords: 1
schema:
type: object
properties:
xp:
type: number
min: 0
_ledger: true
gold:
type: number
min: 0
_ledger: true
playerName:
type: string
kills:
type: number
min: 0
inventory:
type: array
items:
type: object
properties:
itemId:
type: string
quantity:
type: number
min: 0
```

### Sample: Global Leaderboard Collection

Create a file named `leaderboard.collection.yml`:

```yml
sourceVersion: "1"
kind: collection
name: leaderboard
description: Weekly leaderboard entries
collectionType: global
accessMode: endpoint
schema:
type: object
properties:
entries:
type: array
items:
type: object
properties:
steamId:
type: string
name:
type: string
score:
type: number
```

### Sample: Reading with an endpoint

Expose controlled reads through endpoints or queries. Direct collection reads are reserved for secret-key server/backoffice tooling.

```csharp
var player = await NetworkStorage.CallEndpoint( "get-player-profile", new { } );
if ( player.HasValue )
{
var name = player.Value.Str( "playerName", "Unknown" );
var xp = player.Value.Int( "xp", 0 );
Log.Info( $"{name}: {xp} XP" );
}
```

Read and write gameplay data through endpoints so validation, workflows, rate limits, and ledger metadata run server-side. See the [Endpoints](/wiki/network-storage-v3/endpoints) docs.

## Collection definitions vs collection data

There are two different things called "collection operations":

| Operation target | Create/edit/read/remove how? |
|------------------|------------------------------|
| Collection definition/schema | Create and edit in the dashboard or YAML Source + Sync Tool. Delete definitions only from the website dashboard so two-step verification can run. |
| Collection rows/documents | Use endpoints for player-facing gameplay, or the direct row/document API from trusted dedicated servers/backends. See [Collections HTTP API](/wiki/network-storage-v3/collections-http-api). |

Direct row/document helpers are for trusted server/backoffice code with secret-key access, not ordinary game clients:

```csharp
// Create or replace a row/document.
await NetworkStorage.SaveDocument( "players", "76561198000000001", new { playerName = "Ada", xp = 100 } );

// Read it.
var row = await NetworkStorage.GetDocument( "players", "76561198000000001" );

// Edit it with operations.
await NetworkStorage.UpdateDocument( "players", "76561198000000001",
NetworkStorageOperation.Increment( "xp", 25, source: "server", reason: "Reward" ) );

// Remove the row/document. This does not delete the collection definition.
await NetworkStorage.DeleteDocument( "players", "76561198000000001" );
```

Dedicated servers should load secrets with `+network_storage_secret_key sbox_sk_...` and grant only the needed execute scopes.

## Schema

Every collection has a JSON schema that defines accepted fields and their types. Data that does not match the schema is rejected before being saved.

### Schema Types

| Type | Description | Example |
|------|-------------|---------|
| `string` | Text value | `playerName: { type: string }` |
| `number` | Numeric value (integer or float) | `xp: { type: number }` |
| `boolean` | True/false | `pvpEnabled: { type: boolean }` |
| `object` | Nested object with properties, or a free-form map using `additionalProperties` | `stats: { type: object, properties: ... }` |
| `array` | Ordered list of items | `inventory: { type: array, items: ... }` |
| `player` | Steam ID string with auto-attached `_stats` | `owner: { type: player }` |
| `playerSave` | Full player save document (for save slot collections) | Used internally for save slot structure |

### Schema Constraints

| Constraint | Applies To | Description |
|------------|-----------|-------------|
| `min` | `number` | Minimum allowed value |
| `max` | `number` | Maximum allowed value |
| `_ledger` | `number` | Enable immutable audit trail for this field |
| `_unique` | `string`, `number` | Value must be unique across all documents in the collection |
| `additionalProperties` | `object` | Document dynamic object-map values without requiring a fixed `properties` block |

### Example Schema

A typical player data schema. Notice: no `level` field. Level is computed from `xp` using the `xp_per_level` Game Value.

```yml
type: object
properties:
playerName:
type: string
xp:
type: number
min: 0
_ledger: true
gold:
type: number
min: 0
_ledger: true
health:
type: number
min: 0
max: 100
inventory:
type: array
items:
type: object
properties:
itemId:
type: string
quantity:
type: number
min: 0
stats:
type: object
properties:
kills:
type: number
min: 0
deaths:
type: number
min: 0
playTime:
type: number
min: 0
```

### Best Practice: Don't Store Derived Values

Do not store values that can be computed from other stored values. Common examples:

| Don't store | Compute from | How |
|-------------|-------------|-----|
| `level` | `xp` | `floor(xp / xp_per_level)` via Game Values |
| `killDeathRatio` | `kills`, `deaths` | `kills / max(deaths, 1)` in a transform step |
| `totalValue` | `inventory` | Sum item values via endpoint logic |

This keeps your schema simple, avoids stale data, and lets you rebalance by changing Game Values without migrating stored data.

## Save Slots

Collections support save slots via the `maxRecords` setting. When set, each player can store up to `maxRecords` separate documents (save files).

### Configuration

Set `maxRecords` on the collection:

| Setting | Description |
|---------|-------------|
| `maxRecords: 1` | Single save per player (default) |
| `maxRecords: 5` | Up to 5 save slots per player |
| `maxRecords: 10` | Up to 10 save slots |

### Overwrite Protection

Save slots use `_saveId` and `_saveSeq` for overwrite protection:

- **`_saveId`** -- unique identifier for each save slot
- **`_saveSeq`** -- sequence number that increments on every write

When saving, include the `_saveSeq` from the last read. If another write happened in between (the sequence moved forward), the save is rejected with `STALE_SAVE`:

```yml
ok: false
error: STALE_SAVE
message: Save data has been modified since you last loaded it.
currentSeq: 15
yourSeq: 12
```

This prevents race conditions where two game sessions overwrite each other's data.

### Reading Save Slots

Use endpoint pipelines for save-slot reads. Direct document reads require secret-key server/backoffice tooling. A save-slot endpoint can return all saves for the player:

```yml
saves:
- _saveId: slot_1
_saveSeq: 15
playerName: Hero
xp: 45000
lastPlayed: '2026-03-23T10:00:00Z'
- _saveId: slot_2
_saveSeq: 8
playerName: Mage
xp: 22000
lastPlayed: '2026-03-20T18:30:00Z'
```

## Player Key Mode

Set at the project level, Player Key Mode controls how player documents are keyed in per-player collections:

| Mode | Key Format | Description |
|------|-----------|-------------|
| `steamid` | `76561198000001` | Steam ID only (default) |
| `steamid_default` | `76561198000001_default` | Steam ID with `_default` suffix |
| `custom` | Defined per endpoint | Endpoint specifies the key in its write step |

The key mode affects how `{{steamId}}` resolves in endpoint steps. With `steamid_default`, `key: "{{steamId}}"` in a write step automatically appends `_default`.

## player Type

Fields with `type: player` store a Steam ID and automatically get `_stats` attached when read:

```yml
owner:
type: player
```

When you save `owner: "76561198000001"` and read it back, the system attaches public player info:

```yml
owner: '76561198000001'
owner_stats:
displayName: PlayerOne
avatar: https://...
```

This is useful for leaderboards, trade logs, or any feature where you need to display player information alongside stored data. The `_stats` data is populated from Steam and cached -- it is not stored in your collection.

## Player Stats Auto-Attach

When reading from a per-player collection, the system can auto-attach the player's Steam profile information to the response. This is controlled at the collection level.

When enabled, every read response includes:

```yml
xp: 1500
gold: 3200
_playerStats:
displayName: PlayerOne
avatar: https://...
steamId: '76561198000001'
```

This saves you from making a separate API call to look up player display info.

## C# Example: Reading Collection Data

```csharp
var data = await NetworkStorage.CallEndpoint( "get-player-profile", new { } );
if ( !data.HasValue )
return;

var xp = data.Value.Int( "xp", 0 );
var gold = data.Value.Int( "gold", 0 );

// Compute level client-side for display (server computes authoritatively in endpoints)
var xpPerLevel = GameConfig.XpPerLevel; // loaded from Game Values
var level = xp / xpPerLevel;

Log.Info( $"Player data: {xp} XP (level {level}), {gold} gold" );
```