npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

loft-js

v0.1.0

Published

Browser SDK for Loft-hosted apps: identity, AI chat, document DB, realtime channels, and uploads.

Readme

loft-js

A small typed browser SDK for apps hosted on Loft. It wraps the Loft backend so your app can call identity, uploads, a document database, AI chat, and realtime channels without writing fetch and auth by hand. Every call is same-origin to the backend, which holds all credentials and keys server-side, so nothing secret reaches the browser.

Requirements

This runs in the browser, in a page served by Loft. It assumes:

  • A browser environment. It uses fetch, WebSocket, and location, so it does not run under plain Node or during server-side rendering. Guard any SDK call so it only runs on the client.
  • Same-origin hosting. Every request goes to a relative path (/api/...) on the page's own origin, with cookies, so the page must be served by Loft.

Install

Bundle it into your app and upload the result as part of your deployment:

npm install loft-js
import loft from "loft-js";

const me = await loft.user.me();
const { url } = await loft.upload(file);

The package is ESM-only (no CommonJS build). Import it with import, or in a bundler/TypeScript project; require("loft-js") is not supported.

Or include the prebuilt single file with a script tag (no build step), which exposes a global loft. Pin a version so the file cannot change under you:

<script src="https://unpkg.com/[email protected]/dist/loft.js"></script>
<script>
  loft.user.me().then((me) => console.log(me.name));
</script>

API

Identity

const me = await loft.user.me();
// { id: string, email: string, name: string }

id is the stable identity from the auth provider. Use it for ownership checks. email and name are for display and can change, so never key on them.

Document database

loft.db.collection(name) returns a handle to a per-site collection of schemaless documents. Data is scoped to the site the page is served from and isolated from other sites by the server. Pass a type to get typed reads; without one, fields come back as unknown.

const posts = loft.db.collection<{ title: string; pinned?: boolean }>("posts");

const post = await posts.create({ title: "Hello" }); // post.title, post.id, post.creator typed
const one = await posts.get(post.id);                // the doc, or null if absent
const many = await posts.list({ limit: 20 });        // an array of docs
await posts.update(post.id, { pinned: true });       // the updated doc, or null
await posts.delete(post.id);                         // true if removed, false if already gone

Every stored document carries a server-assigned id and a creator, the stable id of the user who created it, stamped server-side from their token. Compare creator to (await loft.user.me()).id to tell whether the current user owns a document.

Pass { ownerOnly: true } when first creating a collection to make it owner-owned: any signed-in user can read and create, but a document can only be updated or deleted by the user who created it, enforced server-side. Without it, the collection is shared: anyone on the site can edit anything, which is right for a co-editing tool. The mode is fixed when the collection is first created.

const notes = loft.db.collection("notes", { ownerOnly: true });

Subscribe to live changes, including other clients' writes. The returned function unsubscribes, and the socket reconnects on its own if it drops.

const stop = posts.subscribe({
  onCreate: (doc) => render(doc),
  onUpdate: (doc) => render(doc),
  onDelete: (id) => remove(id),
  onStatus: (s) => setBanner(s), // "open" | "reconnecting" | "closed"
  onError: (e) => console.warn(e.kind),
});
// later: stop();

Changes that happen while the socket is disconnected are not replayed. If you need every change, re-list on reconnect: call posts.list() again when onStatus reports "open" after a "reconnecting".

Realtime channels

loft.socket.channel(name) joins an ephemeral pub/sub channel scoped to the site. A message sent by one client is delivered to every other client on the same channel. Nothing is stored. Use it for chat, presence, multiplayer cursors, or live notifications. It reconnects on its own, and sends issued before the socket is open are buffered.

const room = loft.socket.channel("lobby", {
  onStatus: (s) => setBanner(s), // "open" | "reconnecting" | "closed"
  onError: (e) => console.warn(e.kind),
});
const off = room.on((msg) => console.log("peer:", msg));
room.send({ hello: "there" });
// later: off(); room.close();

AI chat

loft.ai.chat(messages) sends a conversation to the platform's model and resolves to the assistant's full reply. Keys stay server-side and the model is chosen by the platform.

const reply = await loft.ai.chat([
  { role: "system", content: "You are concise." },
  { role: "user", content: "Explain WebSockets in one line." },
]);

For token-by-token output, loft.ai.stream(messages) returns an async iterable of tokens:

for await (const token of loft.ai.stream(messages)) {
  output.append(token);
}

Break out of the loop (or pass an aborting signal) to stop early and release the connection.

Uploads

loft.upload(file) stores a file server-side and resolves to a URL under /uploads/... that only signed-in users can fetch. Use the URL directly as an <img src>, an href, and so on.

const { url } = await loft.upload(input.files[0]);
await loft.upload(blob, { name: "report.pdf" }); // override the stored filename
await loft.upload.delete(url); // remove it later; idempotent

Errors

Every call rejects with a LoftError on failure. Switch on kind to handle it. status carries the HTTP status when the failure came from a response.

import loft, { LoftError } from "loft-js";

try {
  await loft.user.me();
} catch (e) {
  if (e instanceof LoftError && e.kind === "auth") {
    // not signed in or not permitted
  }
}

| kind | Meaning | |--------------|----------------------------------------------------------------| | auth | Not signed in or not permitted (401, 403). | | not_found | A 404 from a call that does not already model absence as null. | | http | Any other non-2xx response. | | network | The request never reached the server. | | aborted | The request was cancelled through an AbortSignal. | | timeout | An AbortSignal.timeout() elapsed before the request finished. | | stream | A streamed reply ended before it signalled completion. | | parse | The response body was not the shape the SDK expected. |

Note that get and update resolve to null, and delete resolves to false, for a missing document rather than throwing, so a 404 there is a normal result. create and list are not nullable: a 404 from them throws not_found.

Cancellation and timeouts

Every network call takes an optional signal so you can cancel an in-flight request, which matters most for a long upload or a running chat stream. A cancel rejects with a LoftError of kind aborted.

const controller = new AbortController();
const replyPromise = loft.ai.chat(messages, { signal: controller.signal });
controller.abort();
try {
  await replyPromise;
} catch (e) {
  // e is a LoftError of kind "aborted"
}

For a deadline, pass AbortSignal.timeout(ms). It rejects with kind timeout, distinct from a user cancel, so you can retry a timeout without retrying something the user stopped on purpose:

await loft.upload(file, { signal: AbortSignal.timeout(10_000) }); // kind "timeout" if it elapses

Build

pnpm install
pnpm run build

Emits dist/loft.mjs (ES module, used by import) and dist/loft.js (a minified <script> global), each with a source map, plus dist/index.d.ts (types).

License

MIT. See LICENSE.