# SDK reference

```
<script>
  document.addEventListener("DOMContentLoaded", async () => {
    await homespun.ready;


    const row = await homespun.collections.create("todos", { text: "buy milk" });
    console.log("created", row.key);


    homespun.collections.on("todos", (delta) => {
      console.log(delta.kind, delta.row);
    });
  });
</script>
```

Homespun injects this API into every deployed app automatically, there’s nothing to import. Read “Script ordering” below before writing anything that touches `window.homespun` at the top level of your own script.

## Script ordering

[Section titled “Script ordering”](#script-ordering)

The SDK is injected as `<script src="/_hs/sdk.<hash>.js" defer>`. A `defer` script runs after the page has parsed but before `DOMContentLoaded`, and, importantly, **after** any plain, non-deferred inline `<script>` on the page. If your own script references `homespun.*` at its top level (not inside a callback), it runs before `window.homespun` is defined and throws `ReferenceError: homespun is not defined`, silently, with no visible error unless you check the console.

Fix it one of two ways:

* Wrap anything touching `window.homespun` in a `DOMContentLoaded` listener (what every example on this page does). This is the safer default regardless of where your `<script>` tag sits.
* Or mark your own script `defer` too and place it after the SDK’s tag; deferred scripts run in document order, so a later `defer` script always sees a defined `window.homespun`.

Never reference `window.homespun` at the top level of a plain inline `<script>`, it isn’t defined yet when that line runs.

## `homespun.ready`

[Section titled “homespun.ready”](#homespunready)

A `Promise<void>`. Resolves once the session is resolved and every declared collection has been snapshotted into the local, in-browser mirror. `await` it before your first synchronous read. Rejects after a 20-second connection timeout if the realtime connection never establishes.

## `homespun.app`

[Section titled “homespun.app”](#homespunapp)

Read-only, manifest-derived facts about the app itself, kept live:

```
homespun.app.slug; // string
homespun.app.name; // string
homespun.app.description; // string | null
homespun.app.icon; // string | null
homespun.app.visibility; // "private" | "link" | "public"
homespun.app.collections; // { name: string; appendOnly: boolean }[]
```

## `homespun.session`

[Section titled “homespun.session”](#homespunsession)

Who’s looking at the page right now:

```
homespun.session.kind; // "owner" | "member" | "anonymous"
homespun.session.humanId; // string | null, null when anonymous
homespun.session.login(); // full-page redirect to sign in
homespun.session.logout(); // clears the local session and reloads as anonymous
```

`login()` sends the browser to `homespun.dev` to sign in and back; see [Opening a shared app](/people/opening-a-shared-app/) for what a person experiences during that redirect.

Shipping shortly

A `session.displayName` field and a `homespun.members` read API (list the app’s collaborators by name) are in review, not yet in this build. Until they land, treat a row’s `author` id as a stable identifier only, render it directly or attribute changes generically (“a collaborator edited this”) rather than assuming a display name is available.

## `homespun.collections`

[Section titled “homespun.collections”](#homespuncollections)

Every method operates on an in-memory mirror, kept in sync by the realtime connection.

```
homespun.collections.snapshot(name); // HomespunRow[], synchronous, [] before ready
homespun.collections.get(name, key); // HomespunRow | undefined, synchronous
homespun.collections.on(name, handler); // returns an unsubscribe function
homespun.collections.create(name, data); // Promise<HomespunRow>, server-generated key
homespun.collections.upsert(name, key, data); // Promise<HomespunRow>, create-or-return-existing
homespun.collections.update(name, key, data, { ifMatch }); // Promise<HomespunRow>
homespun.collections.delete(name, key, { ifMatch }); // Promise<void>
```

A `HomespunRow` looks like:

```
{
  key: string,
  data: unknown,
  version: number,
  author: { kind: "agent" | "human", id: string },
  createdAt: string,
  updatedAt: string,
}
```

* **`snapshot`** / **`get`** are synchronous reads against the local mirror; call them any time after `homespun.ready` resolves.
* **`on(name, handler)`** delivers `{ kind: "upsert", collection, row }` or `{ kind: "delete", collection, row: { key, deletedAt } }`. Call the returned function to unsubscribe.
* **`create`** always generates the key server-side. **`upsert`** is the one call that accepts a caller-chosen key and never errors on a collision, it just returns the row that’s already there.
* **`update`** and **`delete`** both accept an optional `ifMatch` for optimistic locking. A stale `ifMatch` rejects with a `conflict` error whose `details.current` is the winning row, already folded into your local mirror by the time the promise rejects, so your next read is already correct without waiting on a realtime message.

## `homespun.feed`

[Section titled “homespun.feed”](#homespunfeed)

The raw, unfiltered (or single-collection) change feed, every create/update/delete across the app, in order:

```
const unsubscribe = homespun.feed.on(
  (entry) => {
    console.log(entry.op, entry.collection, entry.key);
  },
  { collection: "todos" }, // optional
);


homespun.feed.cursor; // highest feed sequence number applied locally so far
```

A `FeedEntry` carries `{ seq, op, collection, key, data, author, ts }`. Note the field is **`op`**, not `kind`, `homespun.feed.on` and `homespun.collections.on` intentionally carry different shapes: `feed` is the raw log, `collections.on` is already folded into row deltas.

## Attachments

[Section titled “Attachments”](#attachments)

```
homespun.uploadBlob(file, { filename, mime }); // Promise<AttachmentRef>
homespun.downloadBlob(attachmentId); // Promise<Blob>
homespun.saveBlob(attachmentId, filename); // Promise<void>, triggers a browser save
```

`AttachmentRef` is `{ id, mime, size, filename }`. These three names are kept from an earlier version of the SDK for continuity; they aren’t renamed to `collections`-style verbs.

## Errors

[Section titled “Errors”](#errors)

Every rejected promise is one shape:

```
{ code: string, message: string, status?: number, details?: unknown, retryable: boolean }
```

SDK-local codes: `network_error`, `invalid_args`, `timeout`, `ws_unavailable`. Anything else (like `conflict` from a stale `ifMatch`) passes the relay’s own error code straight through.
