Seed Protocol
Tutorial: build a blog

Creating items

Create items from the models in your schema, check their values, and link a post to its author with a relation.

An item is one record of a model, such as a single post or a single author. This page creates items from the blog schema in Your first schema and links them with a relation. Items are drafts until you publish them: creating them only writes to the device.

The React samples run inside the SeedClientGate from Create the blog app. The gate keeps its children mounted while the client starts, and Seed's hooks wait for the client on their own. Code that runs on mount and calls the SDK directly, such as a useEffect that calls Model.find, should check useIsClientReady() from @seedprotocol/react first. Outside React (a script, a test, a Node process), await client.init(...) before using these APIs.

Create an item

Every item belongs to exactly one model. A create writes the item, its first version and its property values to the local SQLite database, and useItems lists pick up the new item. Pass property values as a plain object whose keys are property names from your schema.

In React: useCreateItem

import { useCreateItem } from '@seedprotocol/react'

function NewAuthorButton() {
  const { createItem, isLoading, error } = useCreateItem()

  const handleClick = async () => {
    const author = await createItem('Author', {
      name: 'Jane Doe',
      bio: 'Writes about local-first software.',
    })
    if (author) console.log('Created', author.seedLocalId)
  }

  return (
    <>
      <button onClick={handleClick} disabled={isLoading}>
        {isLoading ? 'Saving…' : 'Add author'}
      </button>
      {error && <p>{error.message}</p>}
    </>
  )
}

createItem(modelName, values) resolves to the new item once it is saved locally. It doesn't throw: on failure it resolves to undefined and sets error. A call made while another is in flight returns the first call's promise: it resolves to the first call's item, whatever model and values it was given, and creates nothing.

The values are checked before anything is written. A key the model doesn't have, or a value that fails its property's type or validation rules (a title over 255 characters, a word for a Number), fails the create with an ItemCreateValidationError, whose problems lists each one as { property, message, code }. An unknown model name fails with an UnknownModelError. required isn't checked here: drafts can be incomplete until you publish.

Html, Image and File values are checked as they're saved, after the item is written. If one fails its rules, the create still rejects with an ItemCreateValidationError. Any other failure while saving them, such as a File URL that can't be fetched, rejects with an ItemCreateError: its code is 'save_failed', property names the value that wasn't saved, cause holds the save's own error (usually one named OutboxOpFailedError), and seedLocalId is the draft that was removed (Item.create lists its fields). Either way, a create that fails removes everything it wrote, so retrying doesn't leave a second draft.

If two loaded schemas both define a model with the same name, include the schema in the values, as in createItem('Post', { schemaName: 'blog', title: '…' }); see Schema names and model names.

With a model handle

Outside React, look up the model and call its create() method:

import { Model } from '@seedprotocol/sdk'

const Author = await Model.find({ modelName: 'Author', schemaName: 'blog' })
if (!Author) throw new Error('Author model is not loaded')

const author = await Author.create({
  name: 'Jane Doe',
  bio: 'Writes about local-first software.',
})

console.log(author.seedLocalId)

Model.find() reads a model from the database when it isn't loaded yet, and resolves to undefined only when the database has no such model. Use it, not Model.create(), to get a model from your schema: Model.create('Post', 'blog') always makes a new model, and gives it a numbered name like Post 1 if Post already exists.

create() waits until the item is ready before resolving (up to 15 seconds), so always await it. The lower-level Item.create({ modelName: 'Author', … }) does the same thing without a model handle. Its second argument takes { readyTimeout: ms } to change the wait, { waitForReady: false } to resolve without waiting for the item to be ready (it still waits when an Html, Image or File value has to be stored), and { validate: false } to skip the checks above, for data that was already validated or attested elsewhere. createItem takes the same options as its third argument, as in createItem('Author', values, { validate: false }). A model's create() always validates.

Every item gets a seedLocalId when it's created, a short random string that doesn't change. Until you publish, it's the ID you pass to hooks and use in relations; publishing adds a seedUid. See Item identity.

Set a relation

The blog schema defines Post.author as a Relation to Author. Create the author first, then pass its ID as the relation value when creating the post:

import { Model } from '@seedprotocol/sdk'

const Post = await Model.find({ modelName: 'Post', schemaName: 'blog' })
if (!Post) throw new Error('Post model is not loaded')

const post = await Post.create({
  title: 'Hello, world!',
  summary: 'A first draft.',
  published: false,
  author: author.seedLocalId,
})

A relation value is the related item's ID as a string: its seedLocalId or its seedUid. Pass the string itself: an object such as { seedLocalId } is rejected with an ItemCreateValidationError. The SDK stores a reference to the author's item, not a copy of it. create doesn't check that the ID names an existing item, so a mistyped ID is stored as given; take IDs from real items.

Each property is readable as a field on the item, such as post.title, but values come back as strings ('false', not false) and TypeScript doesn't know the fields. What you get back covers reading values, and Following relations loads the author from a post.

A complete React example

This component creates an author, then a post linked to that author:

src/NewPost.tsx
import { useState } from 'react'
import { useCreateItem } from '@seedprotocol/react'

export function NewPost() {
  const { createItem, isLoading, error } = useCreateItem()
  const [title, setTitle] = useState('')

  const handleCreate = async () => {
    if (!title.trim()) return

    const author = await createItem('Author', {
      name: 'Jane Doe',
      bio: 'Writes about local-first software.',
    })
    if (!author) return

    await createItem('Post', {
      title,
      author: author.seedLocalId,
      published: false,
    })

    setTitle('')
  }

  return (
    <div>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
      />
      <button onClick={handleCreate} disabled={isLoading}>
        {isLoading ? 'Saving…' : 'Add post with author'}
      </button>
      {error && <p>Couldn't save: {error.message}</p>}
    </div>
  )
}

Render <NewPost /> in src/App.tsx above <Posts />, and the posts it creates appear in the list from Create the blog app. A real app would reuse existing authors instead of creating one on every submit.

Where to go next

On this page