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:
- A payer — a signer who funds the account's rent, the SOL deposit that keeps data alive on-chain.
- The System Program — the only program allowed to create new accounts and hand their ownership to another program.
- Your program — which declares how many bytes to reserve, based on the struct's fields.
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.