Framework Architecture
What is Dubhe Framework?
The Move contract code generated by generate does not run in isolation — it requires Dubhe Framework, a package already deployed on the Sui network. The Framework is the shared on-chain infrastructure that provides every Dubhe-built DApp with a storage engine, event system, fee metering, access control, and version management.
The relationship between the two layers:
Your DubheConfig (TypeScript)
│
↓ dubhe generate
Your contract code (Move) ──calls──→ Dubhe Framework (deployed on-chain)
│
↓
DappStorage / UserStorage (shared objects)Architecture layers
┌─────────────────────────────────────────────────────────────┐
│ Your DApp package │
│ ┌───────────────────────┐ ┌──────────────────────────┐ │
│ │ sources/systems/ │ │ sources/scripts/ │ │
│ │ hand-written logic │ │ deploy_hook / migrate │ │
│ └───────────────────────┘ └──────────────────────────┘ │
│ │ calls │
│ ┌────────────▼──────────────────────────────────────────┐ │
│ │ sources/codegen/resources/ (auto-generated) │ │
│ │ health.move / stats.move / player_score.move … │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ calls dubhe::dapp_system / dubhe::dapp_service
↓
┌─────────────────────────────────────────────────────────────┐
│ Dubhe Framework (deployed dubhe package) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ System layer dubhe::dapp_system │ │
│ │ · set_record / set_field / delete_record │ │
│ │ · get_field / has_record / ensure_has_record │ │
│ │ · create_user_storage / settle_writes │ │
│ │ · ensure_latest_version / ensure_not_paused │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────────┐ │
│ │ Core layer dubhe::dapp_service │ │
│ │ · DappHub — global framework object │ │
│ │ · DappStorage — per-DApp shared object │ │
│ │ · UserStorage — per-user shared object │ │
│ │ · dynamic_field reads and writes │ │
│ │ · emits SetRecord / DeleteRecord events │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────────────┐
│ Off-chain indexer / SDK (@0xobelisk/sui-client) │
│ · subscribe to SetRecord events │
│ · call get_field to read on-chain data │
└─────────────────────────────────────────────────────────────┘Three storage objects
When the Framework is deployed, it creates a single DappHub as a globally shared object. When your DApp is deployed, a DappStorage is created for your DApp. Each time a user first interacts, they create their own UserStorage.
DappHub (global, one per chain)
└── Framework config, fee parameters, treasury
DappStorage (per DApp, one per package)
└── dapp_key, admin, version, paused flag
└── credit_pool (paid credits)
└── free_credit (expiring free allocation)
└── global resource data
UserStorage (per user per DApp)
└── owner address, session key
└── unsettled write counter
└── per-user resource dataWhy three objects instead of one?
| Old design (single DappHub) | New design (three objects) |
|---|---|
| All DApps contend on one lock | Each DApp and user has their own lock |
Resource keyed by (dapp_key, user_address) string | Resource stored directly in UserStorage |
No resource_account safety | Storage object IS the identity — no address forgery possible |
DappKey: type-level DApp identity
The dapp_key.move generated by generate defines a single empty struct:
public struct DappKey has copy, drop {}This type is your DApp’s unique identity within the Framework. All write and read operations use the fully-qualified type name of DappKey (package_id::dapp_key::DappKey) as a namespace prefix, which ensures:
- Data from different DApps is completely isolated — no collisions are possible.
- Write functions require
DappKeyas an argument, anddapp_key::new()ispublic(package), so external packages cannot forge it to write unauthorized data.
How a generated resource module calls the Framework
Using a simple health: 'u32' config as an example, generate generates health.move. The core write path is:
health::set(user_storage, 100u32, ctx)
│
└─ 1. encode: to_bytes(100u32) → vector<vector<u8>>
└─ 2. call dapp_system::set_record<DappKey>(
dapp_key::new(), ← proves caller is this package
user_storage, ← the user's personal storage object
key_tuple, ← ["health"]
field_names, ← ["value"]
value_tuple, ← [[100u32_bcs]]
false, ← not offchain
ctx
)
└─ 2a. dapp_service::set_user_record(...) ← actually writes to UserStorage dynamic_field
│ └─ emits SetRecord event
│
└─ 2b. user_storage unsettled_count += 1
(fee settled later via settle_writes)The read path calls dapp_system::get_field directly (bypassing fee logic, incurring no fee), returns raw BCS bytes, and deserializes them into native Move types.
The five capabilities of the Framework
1. Zero infrastructure: per-object storage
Developers do not need to define Sui objects or manage UIDs. The Framework provides ready-to-use key-value stores (DappStorage for global data, UserStorage for per-user data). Generated resource modules only need the appropriate storage object to read and write.
2. Real-time off-chain sync: event-driven
Every call to set_record or delete_record automatically emits a structured event:
public struct Dubhe_Store_SetRecord has copy, drop {
dapp_key: String, // which DApp
account: String, // which account / global key
key: vector<vector<u8>>, // which table key
value: vector<vector<u8>>, // new value
}The SDK’s real-time subscription feature is built on these events, allowing off-chain applications to detect on-chain state changes in near real time without polling.
3. Built-in DApp lifecycle management
Through the run() entry point in genesis.move, the contract automatically creates DappStorage on deployment, writing:
- Admin address — the deployer becomes the initial DApp admin.
- Version — starts at 1; bumped via
upgrade_dappafter each upgrade. - Credit allocation — initial free credit granted for new DApps.
The version constant in migrate.move works with ensure_latest_version() to provide a safe upgrade path.
4. Lazy storage fee metering
Unlike immediate per-write fee deduction, Dubhe uses lazy settlement: writes increment an unsettled_count in UserStorage with no immediate charge. Credit deduction happens when settle_writes is called, typically at the start of a subsequent transaction. This design:
- Never aborts user transactions due to insufficient credit (the DApp absorbs the cost).
- Allows batch settlement across many writes.
- Gives the DApp admin flexibility to subsidize certain users.
5. Offchain mode: minimal-cost data publishing
When a resource is configured with offchain: true, the Framework skips the UserStorage write and only emits an event. The data is received by an off-chain indexer and stored in a database. This is extremely valuable for high-frequency update scenarios (real-time game state, logs) — it avoids the higher gas cost of on-chain storage while preserving blockchain auditability.
Deployment flow and Framework interaction
Developer runs dubhe publish
│
↓
1. Sui network deploys your Move package
│
↓
2. genesis::run(dapp_hub, clock, ctx) is called
│
├─ 2a. dapp_system::create_dapp<DappKey>(dapp_key, dapp_hub, name, description, clock, ctx)
│ └─ creates DappStorage, registers DApp metadata, initializes fee state
│
└─ 2b. deploy_hook::run(dapp_storage, ctx)
└─ your custom initialization logic
(e.g., initialize global config singletons)
│
↓
3. DApp is live; DappStorage is shared; users can create their UserStorageGenerated file structure
After running dubhe generate on a project named my_project, the output directory looks like:
src/my_project/
├── Move.toml
└── sources/
├── codegen/ # auto-generated — do not edit
│ ├── genesis.move
│ ├── init_test.move
│ ├── dapp_key.move
│ ├── error.move
│ ├── resources/
│ │ ├── health.move
│ │ └── stats.move
│ └── enums/
│ └── direction.move
├── scripts/
│ ├── deploy_hook.move # editable: post-deploy initialization
│ └── migrate.move # editable: upgrade version tracking
├── systems/ # editable: game / app business logic
└── tests/ # editable: Move unit testsRegeneration rules: sources/codegen/ is fully wiped and regenerated on every generate run. Only user-editable files outside codegen/ (sources/scripts/deploy_hook.move, sources/systems/, sources/tests/, Move.toml) are never overwritten.