DubheSuiSchemasFramework Architecture

Framework Architecture

What is Dubhe Framework?

The Move contract code generated by schemagen 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 schemagen
Your contract code (Move) ──calls──▶ Dubhe Framework (deployed on-chain)


                                    DappHub (shared object)

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_record / get_field / has_record                │  │
│  │  · create_dapp / upgrade_dapp                         │  │
│  │  · charge_fee (write-fee metering)                    │  │
│  └──────────────────────┬────────────────────────────────┘  │
│                         │                                    │
│  ┌──────────────────────▼────────────────────────────────┐  │
│  │  Core layer  dubhe::dapp_service                      │  │
│  │  · DappHub (global shared object — unified store)     │  │
│  │  · ObjectTable<AccountKey, AccountData>               │  │
│  │  · dynamic_field reads and writes                     │  │
│  │  · emits SetRecord / DeleteRecord events              │  │
│  └──────────────────────┬────────────────────────────────┘  │
│                         │                                    │
│  ┌──────────────────────▼────────────────────────────────┐  │
│  │  Built-in resources                                   │  │
│  │  dubhe::dapp_metadata / dapp_fee_state / dapp_fee_config│ │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  Utilities  dubhe::entity_id / bcs / type_info / math │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│        Off-chain indexer / SDK (@0xobelisk/sui-sdk)          │
│  · subscribe to Dubhe_Store_SetRecord events                 │
│  · call get_record / get_field to read on-chain data         │
└─────────────────────────────────────────────────────────────┘

DappHub: unified storage center

When the Framework is deployed, its init() function publishes a single DappHub object as a globally shared object. There is only one DappHub on-chain; every DApp built with Dubhe writes its state into this same object.

DappHub (global shared object)
└── accounts: ObjectTable<AccountKey, AccountData>
    ├── AccountKey { account: "0xABCD...", dapp_key: "my_project::dapp_key::DappKey" }
    │   └── AccountData (dynamic_field container)
    │       ├── key=["health"]  → value=[BCS(100u32)]
    │       └── key=["stats"]   → value=[BCS(50u32), BCS(200u32)]
    ├── AccountKey { account: "0xABCD...", dapp_key: "another_game::dapp_key::DappKey" }
    │   └── AccountData
    │       └── key=["level"]   → value=[BCS(5u32)]
    └── ...

Storage key composition: Each record is located by the (resource_account, dapp_key_type_name) pair, which identifies a unique AccountData container. Within that container, a [TABLE_NAME, ...extra_key_bytes] vector identifies the specific field. This design naturally isolates data from different DApps even though they share the same DappHub.


DappKey: type-level DApp identity

The dapp_key.move generated by schemagen 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::module::DappKey) as a namespace prefix, which ensures:

  • Data from different DApps is completely isolated — no collisions are possible.
  • The package_id() method extracts the current package address directly from the type name, used to locate the resource_account for global resources.
  • Write functions are annotated public(package), so external packages cannot forge a DappKey instance to write unauthorized data.

How a generated resource module calls the Framework

Using a simple health: 'u32' config as an example, schemagen generates health.move. The core write path is:

health::set(dapp_hub, resource_account, 100u32, ctx)

    ├─ 1. encode: encode(100u32) → vector<vector<u8>>

    ├─ 2. call dapp_system::set_record(dapp_hub, DappKey{}, ["health"], [[100u32_bcs]], account, false, ctx)
    │         │
    │         ├─ 2a. dapp_service::set_record(...)   ← actually writes to DappHub dynamic_field
    │         │         └─ emits Dubhe_Store_SetRecord event
    │         │
    │         └─ 2b. charge_fee(...)                 ← deducts storage credits by byte count

    └─ done

The read path calls dapp_service::get_record / get_field directly (bypassing dapp_system, incurring no fee), returns raw BCS bytes, and deserializes them into native Move types in the generated code.


The five capabilities of the Framework

1. Zero infrastructure: unified storage

Developers do not need to define Sui objects, manage UIDs, or handle ObjectTable setup. The Framework provides a ready-to-use key-value store. Generated resource modules only need &mut DappHub to read and write, dramatically lowering the Move development barrier.

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
    key:      vector<vector<u8>>,  // which table
    value:    vector<vector<u8>>,  // new value
}

The SDK’s real-time subscription feature is built on these events, allowing off-chain applications (game frontends, dashboards, indexers) 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 registers the DApp in DappHub on deployment, writing:

  • DappMetadata — name, description, admin address, current version, and the list of authorized package IDs.
  • DappFeeState — initial storage credits, plus cumulative bytes written and operation counts for analytics.

The version constant in migrate.move works with the Framework’s ensure_latest_version() check to provide a safe upgrade path: old package versions can no longer write, forcing users to migrate to the latest version.

4. Storage fee metering

dapp_system::set_record includes automatic fee metering. Each write deducts credits using this formula:

fee = (total_bytes_written × byte_fee + base_fee) × operation_count

Credits are drawn first from the free allocation (free_credit, granted when a new DApp registers) and then from the paid balance once that is exhausted. This lets Dubhe sustainably maintain on-chain infrastructure while reducing early-developer costs through the free allocation.

5. Offchain mode: minimal-cost data publishing

When a resource is configured with offchain: true, the Framework skips the ObjectTable 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 high 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(dapp_hub, DappKey{}, name, description, clock, ctx)
         │       ├─ dapp_metadata::set(...)   ← registers DApp metadata in DappHub
         │       └─ dapp_fee_state::set(...)  ← initializes storage credit account

         └─ 2b. deploy_hook::run(dapp_hub, ctx)
                 └─ your custom initialization logic
                    (e.g., initialize global config, mint initial assets)


3. DApp is live; resource modules are ready for reads and writes

On-chain storage layout example

Using player_stats (a keyed multi-field resource) to illustrate the complete on-chain storage layout:

// DubheConfig
resources: {
  player_stats: {
    fields: { player: 'address', attack: 'u32', hp: 'u32' },
    keys: ['player']
  }
}

After calling set(dapp_hub, resource_account, player_addr, 50u32, 200u32, ctx), the internal DappHub structure is:

DappHub.accounts
└── AccountKey {
        account: resource_account,         // entity address passed by caller
        dapp_key: "0xPKG::dapp_key::DappKey"
    }
    └── AccountData.dynamic_fields
        └── key = [b"player_stats", BCS(player_addr)]
                          ↑                  ↑
                       TABLE_NAME         BCS-encoded keys fields
            value = [BCS(50u32), BCS(200u32)]
                         ↑            ↑
                       attack          hp (non-key value fields, in declaration order)

get_field reads a single field by its index (field_index: u8), avoiding full-record deserialization for efficient partial updates.


Generated file structure

After running dubhe schemagen 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
    │   ├── errors.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 tests

Regeneration rules: Only sources/codegen/resources/ is fully regenerated on every schemagen run. All other files are only created if they do not already exist — user-editable files are never overwritten.