DubheSuiContractsResourcesFramework Architecture

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 data

Why three objects instead of one?

Old design (single DappHub)New design (three objects)
All DApps contend on one lockEach DApp and user has their own lock
Resource keyed by (dapp_key, user_address) stringResource stored directly in UserStorage
No resource_account safetyStorage 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 DappKey as an argument, and dapp_key::new() is public(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_dapp after 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 UserStorage

Generated 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 tests

Regeneration 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.