How does the init function work in Sui Move?
November 5, 2026
init is a module initializer: if a module defines a private function named init with the right signature, Sui calls it automatically, once, in the transaction that publishes the package. It is the usual place to create admin capabilities, share singleton objects, and use the module's one-time witness. It does not run again on package upgrades, not even for modules added by an upgrade.
Example
module book::shop;
public struct ShopOwnerCap has key, store { id: UID }
public struct Shop has key {
id: UID,
sales: u64,
}
fun init(ctx: &mut TxContext) {
// Give the publisher an admin capability.
transfer::transfer(ShopOwnerCap { id: object::new(ctx) }, ctx.sender());
// Create the single shared Shop.
transfer::share_object(Shop { id: object::new(ctx), sales: 0 });
}After sui client publish, the transaction effects list the new ShopOwnerCap (owned by you) and the shared Shop. Every module in the package with an init gets it called during that same publish.
The rules
The bytecode verifier enforces these, and a package that breaks them can't be published:
- It must be named
initand be private (nopublic, nopublic(package)). - It can't be
entry, can't have type parameters, and returns nothing. - It takes one or two parameters: an optional one-time witness first, then the
TxContextlast.
fun init(ctx: &mut TxContext) { /* ... */ }
fun init(otw: SHOP, ctx: &mut TxContext) { /* ... */ }&TxContext is technically allowed, but you almost always want &mut TxContext, since creating objects needs it.
With a one-time witness
If the module defines a struct named after the module in uppercase, with only drop and no fields, Sui passes an instance of it as the first argument:
module book::hero;
public struct HERO has drop {}
fun init(otw: HERO, ctx: &mut TxContext) {
sui::package::claim_and_keep(otw, ctx);
}This is how you claim a Publisher object or create a currency.
What init can't do
- It can't take other arguments, so it can't read existing objects such as the
Clockor another shared object. It only sees theTxContext. - It can't be called again. Nothing in a transaction can invoke it.
- It's not a security boundary on its own. Creating
ShopOwnerCapininitdoesn't stop another function, perhaps added in a later upgrade, from minting more. If uniqueness matters, use a one-time witness or restrict upgrades. - It doesn't run for modules added in an upgrade. If an upgrade needs setup, write a separate function gated by an admin capability and call it after upgrading.
Testing init
init is an ordinary private function to tests in the same module, so you can call it directly:
#[test]
fun test_init() {
let mut ctx = tx_context::dummy();
init(&mut ctx);
assert!(ctx.ids_created() == 2);
}For multi-transaction tests, call init(scenario.ctx()) inside a test_scenario and then next_tx to take the created objects. For an init that takes an OTW, build one with sui::test_utils::create_one_time_witness<HERO>(). Since tests in other modules can't call a private function, a common pattern is a #[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) } wrapper. See unit tests in Sui Move.
Sources
- Module Initializer, The Move Book
- One Time Witness, The Move Book
- Test Scenario, The Move Book