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

@covedb/core

v0.1.6

Published

A multi-user database for apps that have no backend.

Readme

CoveDB

A multi-user database for apps that have no backend.

CoveDB is a TypeScript SDK that gives your web app collaborative, multi-user data storage — without you ever running a server. Users sign in with their Puter accounts. Their data lives in their Puter storage. You write a schema, call create and query, share links, and you're done.

This makes CoveDB particularly well-suited for:

  • Hobbyists and open-source projects — no infrastructure to run or pay for
  • Apps built by AI coding agents — the explicit-grant access model is simple enough that even a small-context agent can reason about it correctly
  • Apps with user-pays AI — Puter provides AI APIs billed to the user's own account, so you can build AI-powered features with zero hosting cost and a single login flow
// Write
const board = db.create("boards", { title: "Launch checklist" }).value;
db.create("cards", { text: "Ship it", done: false, createdAt: Date.now() }, { in: board });

// Read (React)
const { rows: cards } = useQuery<Schema, "cards">(db, "cards", {
  in: board,
  index: "byCreatedAt",
  order: "asc",
});

// Share
const { shareLink } = useShareLink(db, board);

How it works

Every piece of data in CoveDB is a row. A row belongs to a collection defined in your schema, holds typed fields, and has its own identity and access control. You get a RowHandle when you create or fetch a row — that handle is your entry point for reading fields, sharing the row, and subscribing to real-time updates.

Rows can be nested. A collection can declare that its rows live inside a parent collection: a card lives inside a board, and a recentBoard row lives inside the built-in user collection. The parent relationship constrains who can see child rows — gaining access to a parent automatically makes the children visible too.

Access control is explicit-grant only. There are no complex rule expressions to misconfigure. To let another user into a row, you generate a share link and send it to them. They accept it, they're in. That's the whole model.


Install

pnpm add @covedb/core

Using React? Start with @covedb/react.


Schema

Define your collections once. TypeScript infers field types throughout the SDK automatically.

import { collection, defineSchema, field, index } from "@covedb/core";

export const schema = defineSchema({
  boards: collection({
    fields: {
      title: field.string(),
    },
  }),
  recentBoards: collection({
    in: ["user"],
    fields: {
      boardRef: field.ref("boards"),
      openedAt: field.number(),
    },
    indexes: {
      byBoardRef: index("boardRef"),
      byOpenedAt: index("openedAt"),
    },
  }),
  cards: collection({
    in: ["boards"],
    fields: {
      text: field.string(),
      done: field.boolean(),
      createdAt: field.number(),
    },
    indexes: {
      byCreatedAt: index("createdAt"),
    },
  }),
});

export type Schema = typeof schema;
  • collection({ in: [...] })in lists the allowed parent collections.
  • field.string() / .number() / .boolean() / .date() / .ref(collection) — typed fields; chain .optional() or .default(value) as needed
  • index(fieldName) — makes a field queryable with ordering and range filters

Fields are for metadata that you want to query or index. The canonical CRDT pattern is: row fields hold metadata and row refs, while the CRDT document holds the collaborative value state for that row.


Setup

Create one CoveDB instance for your app and pass it an appBaseUrl so that share links point back to your app:

import { CoveDB } from "@covedb/core";
import { schema } from "./schema";

export const db = new CoveDB({ schema, appBaseUrl: window.location.origin });

Auth and startup

Use getSession() to detect whether the current browser already has a Puter session, and call signIn() from a user gesture when it does not:

const session = await db.getSession();

if (!session.signedIn) {
  await db.signIn();        // call this from a button click
}

await db.ensureReady();

Creating rows

// Create a top-level row
const board = db.create("boards", { title: "Launch checklist" }).value;

// Create a child row — pass the parent row or row ref
db.create("cards", { text: "Write README", done: false, createdAt: Date.now() }, { in: board });
db.create("cards", { text: "Publish to npm", done: false, createdAt: Date.now() }, { in: board });

create and update are synchronous optimistic writes. Use .value on the returned receipt when you want the row handle immediately.

To update fields on an existing row:

db.update("cards", card, { done: true });

Querying

CoveDB queries always run within a known scope. For cards, that scope is a board, so you pass in: board. For collections declared as in: ["user"], omitting in means "use the current signed-in user's built-in user row."

Queries never mean "all accessible rows". If a collection is not declared as in: ["user"], omitting in is an error.

Imperative

// `recentBoards` is declared as `in: ["user"]`, so the current user scope is implicit.
const recentBoards = await db.query("recentBoards", {
  index: "byOpenedAt",
  order: "desc",
  limit: 10,
});
// Multi-parent queries run in parallel, then merge and sort their results
const shelf = await db.query("recentBoards", {
  in: [personalHome, sharedHome],
  index: "byOpenedAt",
  order: "desc",
  limit: 20,
});

With React

@covedb/react ships a useQuery hook that polls for changes and re-renders automatically:

import { useQuery } from "@covedb/react";

function CardList({ board }: { board: BoardHandle }) {
  const { rows: cards } = useQuery<Schema, "cards">(db, "cards", {
    in: board,
    index: "byCreatedAt",
    order: "asc",
  });

  return (
    <ul>
      {cards.map((card) => (
        <li key={card.id}>{card.fields.text}</li>
      ))}
    </ul>
  );
}

rows is always a typed array — never undefined. Other hooks in @covedb/react: useRow, useCurrentUser, useShareLink, useAcceptInviteFromUrl, useMemberUsernames, useDirectMembers, and useMutation.

For app boot, prefer useSession(db):

import { useSession } from "@covedb/react";

function AppShell() {
  const session = useSession(db);

  if (session.status === "loading") {
    return <p>Checking session…</p>;
  }

  if (!session.session?.signedIn) {
    return <button onClick={() => void session.signIn()}>Log in with Puter</button>;
  }

  return <App />;
}

Sharing rows with share links

Access to a row is always explicit. There is no rule system to misconfigure — no typo in a policy expression that accidentally exposes everything. A user either holds a valid invite token or they don't.

Sharing is a three-step flow:

// 1. Generate a token for the row you want to share
const token = db.createInviteToken(board).value;

// 2. Build a link the recipient can open in their browser
const link = db.createShareLink(board, token.token);
// → "https://yourapp.com/?db=..."

// 3. Recipient opens the link; your app calls acceptInvite
const board = await db.acceptInvite(link);

acceptInvite accepts either a full invite URL (including the one in window.location.href when the user lands on your page) or a pre-parsed { ref, inviteToken? } object from db.parseInvite(input).

In React apps, useAcceptInviteFromUrl(db, ...) wraps the common invite-landing flow: detect the current invite URL, wait for session resolution, call acceptInvite, optionally await onOpen, and optionally clear the invite params from the address bar after success.

Users who join through an invite token are added as direct "editor" members by default. "viewer" members can view rows but cannot call update() or send CRDT messages.


Membership

Once users have joined a row you can inspect and manage the member list:

// Flat list of usernames
const members = await db.listMembers(board);

// With roles
const detailed = await db.listDirectMembers(board);
// → [{ username: "alice", role: "editor" }, ...]

// Add or remove manually
await db.addMember(board, "bob", "editor").committed;
await db.removeMember(board, "eve").committed;

Membership inherited through a parent row is visible via listEffectiveMembers.


Real-time sync (CRDT)

CoveDB includes a CRDT message bridge. Connect any CRDT library to a row and all members receive each other's updates in real time.

Sending CRDT updates requires "editor" access, but all members can poll and receive them.

Here is the recommended Yjs integration:

import * as Y from "yjs";
import { createYjsAdapter } from "@covedb/yjs";
import { useCrdt } from "@covedb/react";

const adapter = createYjsAdapter(Y);
const { value: doc, flush } = useCrdt(board, adapter);

// Write to doc normally, then push immediately when needed
await flush();

@covedb/yjs uses your app's yjs instance instead of bundling its own runtime, which avoids the multi-runtime Yjs failure mode.


Example apps

packages/todo-app is the code from this README assembled into a working app — boards, recent boards, cards, and share links. Run it with:

pnpm --filter todo-app dev

For a fuller picture of how the pieces fit together in a real app, read packages/woof-app. It uses CRDT-backed live chat, user-scoped history rows for room restore, child rows with per-user metadata, and role-aware UI — the patterns you'll reach for once basic reads and writes are working.

pnpm --filter woof-app dev

API reference

CoveDB

| Method | Description | |--------|-------------| | new CoveDB({ schema, appBaseUrl? }) | Create a CoveDB instance. Pass appBaseUrl so share links point back to your app. | | ensureReady() | Explicitly await authentication and provisioning before mutations. Recommended during app startup. | | whoAmI() | Returns { username } for the signed-in Puter user. | | create(collection, fields, options?) | Create a row optimistically and return a MutationReceipt<RowHandle> immediately. Pass { in: parent } for child rows, where parent can be a RowHandle or RowRef; for collections declared as in: ["user"], omitting in uses the current signed-in user's built-in user row. Most apps use .value; await .committed when you need remote confirmation. | | update(collection, row, fields) | Merge field updates onto a row optimistically and return a MutationReceipt<RowHandle> immediately. row can be a RowHandle or RowRef. | | getRow(row) | Fetch a row by typed reference. | | query(collection, options) | Load rows under a parent, with optional index, order, and limit. For collections declared as in: ["user"], omitting in uses the current signed-in user's built-in user row. | | watchQuery(collection, options, callbacks) | Subscribe to repeated query refreshes via callbacks.onChange. For collections declared as in: ["user"], omitting in uses the current signed-in user's built-in user row. Returns a handle with .disconnect(). | | createInviteToken(row) | Generate a new invite token for a row and return a MutationReceipt<InviteToken>. Most apps can use .value immediately. | | getExistingInviteToken(row) | Return the existing token if one exists, or null. | | createShareLink(row, token) | Build a shareable URL containing a serialized row ref and token. | | parseInvite(input) | Parse an invite URL into { ref, inviteToken? }. | | acceptInvite(input) | Join a row via invite URL or parsed invite object, and return its handle. Invite joins become direct "editor" members by default. | | saveRow(key, row) | Persist one current row for the signed-in user under your app-defined key. | | openSavedRow(key) | Re-open the saved row for the signed-in user, or null. | | clearSavedRow(key) | Remove the saved row for the signed-in user. | | listMembers(row) | Returns string[] of all member usernames. | | listDirectMembers(row) | Returns { username, role }[] for direct members. | | listEffectiveMembers(row) | Returns resolved membership including grants inherited from parents. | | addMember(row, username, role) | Grant a user access and return a MutationReceipt<void>. Roles: "editor" and "viewer". "editor" can update fields, manage members, manage parents, and send CRDT messages; "viewer" is read-only. | | removeMember(row, username) | Revoke a user's access and return a MutationReceipt<void>. | | addParent(child, parent) | Link a row to an additional parent after creation and return a MutationReceipt<void>. | | removeParent(child, parent) | Unlink a row from a parent and return a MutationReceipt<void>. | | listParents(child) | Returns all parent references for a row. | | connectCrdt(row, callbacks) | Bridge a CRDT onto the row's message stream. Returns a CrdtConnection. |

RowHandle

| Member | Description | |--------|-------------| | .fields | Current field snapshot, typed from your schema. Treat it as read-only; the object is replaced when fields change. | | .collection | The collection this row belongs to. | | .ref | Portable RowRef object for persistence, invites, ref-typed fields, and reopening the row later. | | .id / .owner | Row identity metadata. | | .refresh() | Re-fetch fields from the server. Resolves to the latest field snapshot. | | .connectCrdt(callbacks) | Shorthand for db.connectCrdt(row, callbacks). | | .in.add(parent) / .in.remove(parent) / .in.list() | Manage parent links. | | .members.add(username, { role }) / .members.remove(username) / .members.list() | Manage membership. |

MutationReceipt<T>

| Member | Description | |--------|-------------| | .value | The optimistic value available immediately. For create and update, this is the RowHandle. | | .committed | Promise that resolves to the final value once the write is confirmed remotely. Rejects if the write fails. | | .status | Current write status: "pending", "committed", or "failed". | | .error | The rejection reason after a failed write. Otherwise undefined. |