Free guide

The Solana account model, explained

Solana has no database. Every byte of state lives in an account: owned by one program, fixed in size, readable by anyone. Here is the model, and the Anchor code that drives it.

Storage without a database

A Solana program holds no state between calls. Everything it wants to remember goes into accounts: chunks of on-chain storage, each owned by exactly one program, each with a byte size fixed when the account is created. Your program defines the shape of that storage as a plain Rust struct, and Anchor's #[account] macro handles turning the struct into bytes and back.

Ownership gates writing, not reading. Only the owning program's instructions can change an account's data, but every program, indexer, and RPC client can read it, permanently. The official example this guide follows stores a street address as its demo payload — worth remembering when you decide what belongs on-chain.

Three parties create every account

No account creates itself. Creation takes three parties working together:

Keep this picture; every account-creation pattern in the ecosystem, program-owned vaults included, is a variation on it.

The shape of the data

The official account-data example stores one record per account. Its state file is the whole data model:

// state/address_info.rs
#[account]
#[derive(InitSpace)] // automatically calculate the space required for the struct
pub struct AddressInfo {
    #[max_len(50)] // set a max length for the string
    pub name: String, // 4 bytes + 50 bytes
    pub house_number: u8, // 1 byte
    #[max_len(50)]
    pub street: String, // 4 bytes + 50 bytes
    #[max_len(50)]
    pub city: String, // 4 bytes + 50 bytes
}

#[account] is what makes the struct a valid account type. #[derive(InitSpace)] generates an AddressInfo::INIT_SPACE constant from the field sizes. The #[max_len(50)] attributes exist because the size is fixed up front: a String cannot grow past what you declare here. One more number matters — every Anchor account reserves 8 extra bytes at the front, the discriminator, which tags which struct type the account holds so one type cannot be misread as another. The example keeps that as its single constant, ANCHOR_DISCRIMINATOR_SIZE.

Creating one

The instruction that creates the account lists three accounts, matching the three parties above:

// instructions/create.rs
#[derive(Accounts)]
pub struct CreateAddressInfo<'info> {
    #[account(mut)]
    payer: Signer<'info>,

    #[account(
        init,
        payer = payer,
        space = ANCHOR_DISCRIMINATOR_SIZE + AddressInfo::INIT_SPACE,
    )]
    address_info: Account<'info, AddressInfo>,
    system_program: Program<'info, System>,
}

The init constraint triggers the creation, with space spelling out the fixed size: discriminator plus fields. Undersize it and the first write that overruns fails the whole transaction; there is no silent truncation. With validation and creation already handled by the struct, the handler's only job is to fill in the data:

// instructions/create.rs
pub fn create_address_info(
    ctx: Context<CreateAddressInfo>,
    name: String,
    house_number: u8,
    street: String,
    city: String,
) -> Result<()> {
    *ctx.accounts.address_info = AddressInfo {
        name,
        house_number,
        street,
        city,
    };
    Ok(())
}

From solana-foundation/program-examples, MIT licensed, © Solana Foundation, pinned to commit 9389865.

Note what is absent: no manual byte-serialization, no explicit write. Assign the struct to *ctx.accounts.address_info and Anchor turns it back into bytes when the instruction finishes.

Who signs for the address

Where does the new account's address come from? In this example, from a keypair the client generates before the transaction is sent:

// Generate a new keypair for the addressInfo account
const addressInfoAccount = new Keypair();

The test then passes that keypair as a co-signer alongside the payer:

await program.methods
  .createAddressInfo(addressInfo.name, addressInfo.houseNumber, addressInfo.street, addressInfo.city)
  .accounts({
    addressInfo: addressInfoAccount.publicKey,
    payer: payer.publicKey,
  })
  .signers([addressInfoAccount])
  .rpc();

The signature is not decoration. An account at a keypair-supplied address can only be created with a signature from the keypair that address belongs to, so the client must hold the private key and co-sign. The alternative, an address a program derives and signs for itself with no keypair anywhere, is the Program Derived Address.

Where the model goes from here

Everything else in Solana programming stands on this picture. Validating who may touch an account, closing one and reclaiming its deposit, resizing one after creation, deriving its address from seeds instead of a keypair: each is the same three-party creation story with one part swapped. Learn this shape once and the rest reads as variations.