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”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.homespunin aDOMContentLoadedlistener (what every example on this page does). This is the safer default regardless of where your<script>tag sits. - Or mark your own script
defertoo and place it after the SDK’s tag; deferred scripts run in document order, so a laterdeferscript always sees a definedwindow.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”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”Read-only, manifest-derived facts about the app itself, kept live:
homespun.app.slug; // stringhomespun.app.name; // stringhomespun.app.description; // string | nullhomespun.app.icon; // string | nullhomespun.app.visibility; // "private" | "link" | "public"homespun.app.collections; // { name: string; appendOnly: boolean }[]homespun.session
Section titled “homespun.session”Who’s looking at the page right now:
homespun.session.kind; // "owner" | "member" | "anonymous"homespun.session.humanId; // string | null, null when anonymoushomespun.session.login(); // full-page redirect to sign inhomespun.session.logout(); // clears the local session and reloads as anonymouslogin() sends the browser to homespun.dev to sign in and back; see
Opening a shared app for what a person experiences during that
redirect.
homespun.collections
Section titled “homespun.collections”Every method operates on an in-memory mirror, kept in sync by the realtime connection.
homespun.collections.snapshot(name); // HomespunRow[], synchronous, [] before readyhomespun.collections.get(name, key); // HomespunRow | undefined, synchronoushomespun.collections.on(name, handler); // returns an unsubscribe functionhomespun.collections.create(name, data); // Promise<HomespunRow>, server-generated keyhomespun.collections.upsert(name, key, data); // Promise<HomespunRow>, create-or-return-existinghomespun.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/getare synchronous reads against the local mirror; call them any time afterhomespun.readyresolves.on(name, handler)delivers{ kind: "upsert", collection, row }or{ kind: "delete", collection, row: { key, deletedAt } }. Call the returned function to unsubscribe.createalways generates the key server-side.upsertis the one call that accepts a caller-chosen key and never errors on a collision, it just returns the row that’s already there.updateanddeleteboth accept an optionalifMatchfor optimistic locking. A staleifMatchrejects with aconflicterror whosedetails.currentis 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”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 farA 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”homespun.uploadBlob(file, { filename, mime }); // Promise<AttachmentRef>homespun.downloadBlob(attachmentId); // Promise<Blob>homespun.saveBlob(attachmentId, filename); // Promise<void>, triggers a browser saveAttachmentRef 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”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.