Resources
What are Resources?
In Dubhe, resources are the typed on-chain storage layer. When building with Dubhe, developers declare all contract state under the resources key in dubhe.config.ts. The generate CLI then produces Move modules that handle storage reads, writes, and events automatically.
This replaces the traditional Move approach of defining data structures and table access manually:
Traditional Move storage
// Declare manually
struct Player has store {
level: u32,
hp: u64,
}
// Store and retrieve manually
let player = Player { level: 1, hp: 100 };
table::add(&mut storage, addr, player);
let p = table::borrow(&storage, addr);Dubhe resources (dubhe.config.ts)
// dubhe.config.ts — declare once
resources: {
level: 'u32',
hp: 'u64',
}// Generated Move — type-safe, no boilerplate
level::set(&mut user_storage, 1, ctx);
let lv = level::get(&user_storage);All storage operations emit standardized events that enable zero-code indexing and off-chain data synchronization.
Core storage model
Resources use two kinds of storage objects backed by Dubhe’s shared infrastructure:
| Storage object | Used for | One per |
|---|---|---|
UserStorage | Per-user data (default) | User per DApp |
DappStorage | DApp-wide data (global: true) | DApp |
Generated resource modules accept the appropriate storage object directly — there is no resource_account string parameter. The storage object itself is the user or DApp identity.
UserStorage (user's personal store)
└── level, hp, inventory, … (per-user resources)
DappStorage (DApp-wide store)
└── total_players, config, … (global resources)Concurrency model:
UserStoragescales linearly. Each user’sUserStorageis a separate shared object. Concurrent transactions from different users never block each other — throughput grows with the number of active users.DappStoragewrites serialize. There is only oneDappStorageper DApp. Any transaction that takes&mut DappStoragemust be sequenced through consensus. Useglobal: trueonly for infrequently updated data such as config flags or counters updated once per session. Avoid placing high-frequency write paths on global resources.
Resource patterns
There are six patterns depending on the combination of global, keys, and fields:
| Pattern | Config shape | Storage object | Primary use case |
|---|---|---|---|
| Global singleton | global: true | DappStorage | Package-level counters, config flags |
| User single value | 'MoveType' shorthand | UserStorage | Simple per-player properties |
| User multi-field record | fields only | UserStorage | Structs keyed by user |
| Keyed single value | fields + keys (1 value field) | UserStorage | Maps like (item_id) → quantity |
| Keyed multi-value record | fields + keys (multiple value fields) | UserStorage | Rich indexed data |
| Offchain | offchain: true | UserStorage | Events only, no on-chain storage |
See Config Reference for the full API generated by each pattern.
Enum and error support
Custom enum types can be declared alongside resources and used as field or key types. Each enum compiles to a Move module with constructors, matchers, and BCS helpers.
Custom error codes compile to assertion helpers that abort with a message string, making contract revert reasons immediately readable.
Why resources?
Resources address several hard problems in on-chain application development:
- Separation of state from logic — business logic lives in system contracts; all state lives in storage objects. Upgrading logic never migrates data.
- Object-level isolation — each user’s
UserStorageis a separate shared object, so concurrent user transactions never contend on a single lock. - Automatic sync to frontends — all writes emit standardised events that a Dubhe indexer can consume without any extra code.
- Cross-contract queries — any contract that receives a
UserStoragecan read data from that user’s store. - Gas efficiency — values are tightly packed in the underlying storage, often cheaper than hand-written table access.