Schemas, Models, Items, Properties
How schemas, models and model properties define what an app can store, and how items and item properties hold the records.
If you have been following Getting started or the tutorial, you have already touched every layer of Seed Protocol's data model: a schema file declared a Post model, and you created an item with a title property.
The hierarchy at a glance
Schemas, models and model properties are the structure: they describe what an app can store. Items and item properties are the data: the records you create, edit and publish. In a blog schema with Post and Author models, two posts by one author look like this:
| Term | Layer | What it is |
|---|---|---|
| Schema | Structure | A named, versioned container for your app's content types. Identified by metadata.name (for example blog). |
| Model | Structure | A content type inside a schema (for example Post, Author). Declares a set of property definitions. |
| Model property | Structure | A typed field on a model: its name, data type, and options like required or validation rules. |
| Item | Data | One record of a model: a single post, author, or image. |
| Item property | Data | One field value on an item: the actual string, number, relation ID, or file reference stored for that record. |
In day-to-day app code you mostly work with schemas (via seed.schema.json), models (via hooks like useModel), and items (via useItem, useItems, and direct property assignment). Model properties and item properties become important when you evolve schemas, build tooling, or need fine-grained reactivity to a single field.
Schema
A schema is the top-level contract for a domain of content. It has a name, a version number, and a set of models. Most apps have one schema; larger projects might split concerns across several (for example blog and settings).
Schemas are written as JSON in Seed Protocol's data model format. A typical project keeps the file in the project and passes it to the client as config.schema:
my-seed-app/
seed.schema.json ← schema definitions
src/When the client initializes, it writes the schema's models and properties into the local SQLite database. From then on, items you create and save use those definitions: creating an item with a name the model doesn't declare fails, and each value is checked against its property's type and validation rules before it's written.
The models and properties are also the shared contract when content is published. On-chain, a published item is identified by its model name (a Post is attested under the EAS schema bytes32 post), and each property value by its type and name (string title). The app's schema name isn't recorded on-chain, so any app whose Post model has a title of type Text can read the same items. That is why model names, property names and types matter beyond your own app; What's published where has the naming rule and how sync matches types.
Schemas and models can also be made in code: Schema.create loads a schema by name or creates an empty one, and Model.create adds a new model to it.
Model
A model is a content type: the Seed equivalent of a table, collection, or document type. Model names are PascalCase by convention (Post, not post) and unique within a schema.
Each model carries:
- A name, which you pass to
useCreateItem,useItems, andModel.find - An optional description, kept in the schema file for people and tooling
- A properties map: the fields every item of this model can have
"Post": {
"description": "A blog post",
"properties": {
"title": { "type": "Text", "required": true },
"body": { "type": "Html" },
"published": { "type": "Boolean" },
"author": { "type": "Relation", "model": "Author", "required": true }
}
}A model describes shape only. Creating a model definition doesn't create any items; you do that separately once the schema is loaded.
Model property
A model property is the schema-side definition of a single field: its name, type, and options (required, validation rules, relation and list targets). The schema file and the code APIs spell some options differently (type in the file is dataType in code); Property definition reference lists both.
The SDK exposes model properties as ModelProperty instances when you work with schema editing APIs or hooks like useModelProperties and useModelProperty. For most read/write app code, you interact with items directly (item.title) rather than model property objects, but the model property is what gives that field its type and rules.
Property types
Every model property has one of ten types, and every type except List holds a single value. Data types lists them with what each one accepts and reads back as.
Html, Image and File values, and Text values with ItemStorage storage, are kept in files locally and uploaded to Arweave when you publish; other values are attested directly with EAS. See File and image properties and Permanence.
Item
An item is one record of a model: the thing you create, list, edit, and publish. If Post is the model, each blog post you write is an item.
Items are always local first. Item.create queues the new item in the outbox and resolves once the leader tab has written it to the local SQLite database in one transaction, so when it resolves, every tab can load the item. Nothing is signed or uploaded until you publish.
import { Model, type IItem } from '@seedprotocol/sdk'
type Post = IItem<any> & { title?: string; published?: boolean | string }
const postModel = await Model.find({ modelName: 'Post', schemaName: 'blog' })
if (!postModel) throw new Error('Post model is not loaded')
const post = (await postModel.create({
title: 'Hello, world!',
published: false,
})) as Post
post.title = 'Hello, world! (revised)'
post.published = trueEvery item belongs to exactly one model (and, through it, one schema). The SDK adds a field to the item for each property of its model. Assigning to one, like post.title = '…', saves the new value in the background after validating it against the property's type and rules. These fields are added at runtime, so in TypeScript you need a cast to use them; see Typing items in TypeScript.
In React, items surface through hooks such as useItem, useItems, and useCreateItem. See Creating items and Reading and writing data.
Item property
An item property is the runtime value of one field on one item. When you read post.title, you are reading an item property. When you assign post.title = 'New title', you are updating one.
Most of the time you work with items as plain objects (item.title, item.publishedAt) and the SDK handles persistence and reactivity. The ItemProperty class and useItemProperty hook exist for subscribing to a single field without re-rendering on every change to the item:
import { useItemProperty } from '@seedprotocol/react'
function PostTitle({ seedLocalId }: { seedLocalId: string }) {
const { property } = useItemProperty({ seedLocalId, propertyName: 'title' })
return <h2>{property?.value}</h2>
}You can also walk item.properties, an array of ItemProperty instances, each with a propertyName and a value, when building generic editors or debugging.
How structure and data connect
Creating an item checks its values against the model, and if a value fails, nothing is written. required isn't checked when you save locally; publishing checks it on Relation, Image, File, Html and Json properties. Validation lists the errors Item.create throws.
How a write travels from assignment to SQLite is on Items, actors and the write path, and how React re-renders stay in sync is on The reactivity model.
Item identity
Every item carries identifiers at two stages of its life:
| Field | When it exists | Use for |
|---|---|---|
seedLocalId | Always, for local items | Keys in lists, useItem, relations between unpublished items |
seedUid | Once the item has a Seed attestation on EAS (after you publish it, or when it was synced from EAS) | Cross-app references, EAS explorer links, permanent identity |
Until you publish, seedLocalId is the ID that matters, and relations between local items store it. After publish, the item gains a stable seedUid: the on-chain Seed attestation that persists across versions and outlives any single app.
Publishing doesn't replace the local record. It adds the public identity and attested property values on top of what is already in SQLite. See How Seed Protocol works for the Seed → Version → Property attestation hierarchy.
Relations between items
A Relation property stores another item's ID as a string. Before you publish, that's the related item's seedLocalId. Publishing the item rewrites the stored value to the related item's seedUid, the value its attestation holds and an EAS sync stores; a List of relations becomes a JSON array of seedUids the same way. Code that follows a relation has to accept either kind of ID; see Following relations.
Relations are always stored on the "many" side of a one-to-many relationship. Each Post points to its Author; there is no posts list on Author:
"Post": {
"properties": {
"title": { "type": "Text" },
"author": { "type": "Relation", "model": "Author", "required": true }
}
}A publication with thousands of posts doesn't need to maintain a growing list on the parent. To find all posts for an author, load Post items and filter by the author property. There is no built-in inverse relation. See Modeling relations for lists of relations, Image relations, and query strategies.
Naming conventions
| Thing | Convention | Example |
|---|---|---|
| Schema name | short and stable | blog |
| Model name | PascalCase | Post, Author |
| Property name | camelCase | publishedAt, wordCount |
The schema name (metadata.name) is what you pass to schema-scoped APIs such as useModels('blog') and useModel('blog', 'Post'). APIs that take a model name usually don't need it, but if two loaded schemas define the same model name, pass schemaName as well (for example useItems({ modelName: 'Post', schemaName: 'blog' })). Some property names are reserved for fields the SDK manages, such as createdAt, updatedAt and type; see Your first schema.
Evolving the model
Schemas are versioned through the version field. When a schema is passed as config.schema, the client applies it at start whenever it changed since it was last applied, adding new models and properties to the local database, and skips it otherwise.
Treat published models as public contracts. Prefer adding new properties over renaming or removing existing ones once data is on-chain. Readers and other clients depend on stable field names and types.
During development, edit seed.schema.json, raise version, and reload the app. Raise it with every edit: the SDK keeps a copy of each version in its files directory, and an edit at the same version replaces that version's copy, so the previous one is gone. The SDK applies the edit anyway and logs a warning that asks you to raise version.
Changing a schema once you have real data, including safe renames and type changes, is covered in Change a schema after you've shipped.
Where to go next
- Your first schema: write a schema file from scratch, with property types and a multi-model example
- Local-first and offline behavior: why items live in SQLite first and what that means for your app
- Conflict detection: how the SDK prevents silent overwrites when the same record changes in two places
Publishing your first item
Install @seedprotocol/publish, connect a wallet, and publish a blog post from the local database to Arweave and EAS on the OP Sepolia testnet.
Local-first and offline
Why the local database is your app's source of truth, what works without a network and what doesn't, how EAS sync brings published data in, and how publishing survives a reload.