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

snapback4

v0.4.9

Published

Snapback 4: the card (contract), the online and local-first clients (IndexedDB, SQLite), React hooks, sign-in, a labelled mock, and the `snapback4` CLI. LLP 3000.

Readme

snapback4

A scale-to-zero backend: Quarry declares your data and rules; typed clients read online or on a device, using the same Rust interpreter as the server. The package carries the TypeScript client and the CLI (currently darwin-arm64).

Start here: your first working account screen

Follow this README.md through password signup, profile, avatar and reopen. Coming from SQL or Convex: npx snapback4 guide from-sql (aliases: sql, convex, rosetta). For messaging apps: npx snapback4 new my-social --template social installs the Expo Router starter; guide social explains typing, once-media and revalidation.

1. Create and run the backend

Use Node 24. In a new app directory, install the exact package and Expo starter:

npm install --save-exact snapback4
npx snapback4 init --expo
npm install

Use init --web for the offline Vite starter, init for a schema, or init --online for an online-only app. The example below uses the Expo starter's installed React Native and SecureStore. Replace snapback/schema.q, src/client.ts and App.tsx with the blocks below, and remove the unused src/Inbox.tsx, src/Thread.tsx and src/witness.ts chat screens. Remove the chat's optional snapback/worker.mts. Then run npx snapback4 check to generate the API; keep npx snapback4 dev running in one terminal and npm start in another (Expo Go), or use npm run ios:dev for a native development build. Do not set a persona for this password example. The iOS simulator reaches http://127.0.0.1:4400; set EXPO_PUBLIC_SNAPBACK_URL to your TLS deployment for a physical phone.

2. Declare identity and a profile

An email-and-password product declares use identity password; plain use identity also mints guest sessions. Password signup requires at least 8 characters (Unicode scalar values) and at most 1,024 UTF-8 bytes; login accepts at most 1,024 UTF-8 bytes. Sessions normally last seven days. Handles are discoverable; profiles and avatars are private to their owner. profileFor explicitly refuses another owner; table rules otherwise hide unreadable rows as missing. Handle claims are server-authoritative.

use identity password
table profiles:
  principal: principal
  handle: text 3..20 format handle
  avatar: image <=10000000 ?
  unique principal
  unique handle
  read <- .principal = viewer
  insert <- .principal = viewer
  update <- .principal = viewer
  immutable principal
  cap 1 by principal
  sync to .principal
table handles:
  principal: principal
  handle: text 3..20 format handle
  unique principal
  unique handle
  public 'handles are discoverable after signup'
  insert <- .principal = viewer
  update <- .principal = viewer
  immutable principal
  online only
query handleAvailable(handle: text 3..20 format handle):
  return not exists handles[handle = handle]
query myProfile():
  return profiles[principal = viewer]
query profileFor(owner: principal):
  require owner = viewer else PROFILE_PRIVATE
  return profiles[principal = owner]
mutation saveProfile(handle: text 3..20 format handle):
  upsert handles[principal = viewer] { principal: viewer, handle }
  upsert profiles[principal = viewer] { principal: viewer, handle }
mutation placeAvatar(avatar: image <=10000000):
  update profiles[principal = viewer] { avatar }

A post-signup availability check uses the new session. Incomplete or stale availability is not permission to claim a handle. Availability and handle saves use the server; your private profile is held locally. The unique index decides races.

3. Create the app

Replace src/client.ts: the app restores the session, opens its client and owns pending writes across restarts. Native sessions use SecureStore; browser persistence is explicit because page scripts can read localStorage. Keep the starter's Metro config and wasm transformer. assets retains viewed media within its device byte budget; Expo Go uses the online client and ignores it.

For an own Release build and native smoke, run npx snapback4 witness ios --build . --smoke --udid "$UDID" --artifacts artifacts/native-smoke. The bounded Expo 57.0.22 / RN 0.86.3 / Xcode 27 adapter installs and launches without GUI automation or Metro. Read npx snapback4 guide witness for exact pins, helper/session requirements, immutable proofs, output restrictions, installed-app mode and deeper scenarios. After edits, repeat --build with fresh artifacts; the private Xcode cache stays warm. Repack is withdrawn. Five complete warm runs on the shared M5 Pro mini passed: 159.841 s median / 197.562 s p95 (2.7 / 3.3 minutes). Cold native preparation took about six minutes, plus smoke and tool/dependency setup; these are pinned-app observations, not timing guarantees. Compiler-declared server-only reads must answer online and record expected offline unavailability. A build receipt alone proves neither smoke nor M6/M8 application behavior.

// src/client.ts
import { Platform } from "react-native";
import * as SecureStore from "expo-secure-store";
import { createApp } from "snapback4/app";
import { localStorageSessionStore } from "snapback4/auth";
import { nativeAvailable, nativeDevice } from "snapback4-expo";
import { webDevice } from "snapback4/web";
const sessionStore = {
  get: (key: string) => SecureStore.getItemAsync(key),
  set: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  delete: (key: string) => SecureStore.deleteItemAsync(key),
};
export const app = createApp({
  url: process.env.EXPO_PUBLIC_SNAPBACK_URL ?? "http://127.0.0.1:4400",
  sessionStore: Platform.OS === "web" ? localStorageSessionStore() : sessionStore,
  device: Platform.OS === "web" ? webDevice() : nativeAvailable() ? nativeDevice : undefined,
  assets: { keep: "viewed", maxBytes: 67108864 },
});

4. Sign in, save and render

Replace App.tsx. A refused sign-in returns { ok: false, why }. A missing profile resumes onboarding; a denied read remains an error. Enter an image URI accessible to the host (on web its server must allow CORS), or pass a photo picker's Blob to upload. Uploads require a connection; placement is a mutation.

// App.tsx
import { useState } from "react";
import { Button, Image, Text, TextInput, View } from "react-native";
import { AppProvider, useAuth, useClient, useQuery, useMutation, useAsset } from "snapback4/react";
import { app } from "./src/client";
import { api } from "./snapback/generated/api";
export function App() { return <AppProvider app={app}><Account /></AppProvider>; }
function Account() {
  const auth = useAuth();
  const [email, setEmail] = useState(""), [password, setPassword] = useState("");
  const [problem, setProblem] = useState("");
  if (auth.state === "restoring") return <Text>Restoring account…</Text>;
  if (auth.state === "signed-in") return <Profile />;
  const signIn = async (mode: "signup" | "login") => {
    const result = await (mode === "signup" ? auth.signup(email, password) : auth.login(email, password));
    if (!result.ok) setProblem(result.why.message);
  };
  return <View style={{ padding: 32, gap: 12 }}>
    <Text>{problem || auth.why?.message || "Sign up or log in"}</Text>
    <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
    <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
    <Button title="Sign up" onPress={() => void signIn("signup")} />
    <Button title="Log in" onPress={() => void signIn("login")} />
  </View>;
}
function Profile() {
  const auth = useAuth(), client = useClient();
  const profile = useQuery(api.myProfile, {}), save = useMutation(api.saveProfile), place = useMutation(api.placeAvatar);
  const avatar = useAsset("data" in profile ? profile.data?.avatar?.id : null);
  const [handle, setHandle] = useState(""), [uri, setUri] = useState(""), [problem, setProblem] = useState("");
  const busy = save.inFlight || place.inFlight;
  const upload = async () => {
    try {
      const result = await client.upload(await (await fetch(uri)).blob());
      if (!result.ok) { setProblem(result.why.message); return; }
      await place.run({ avatar: { id: result.asset.id } });
    } catch (error) { setProblem(String(error)); }
  };
  return <View style={{ padding: 32, gap: 12 }}>
    <Text>{"denied" in profile ? profile.denied.message : "loading" in profile ? "Loading…" : profile.data ? `@${profile.data.handle}; avatar ${profile.data.avatar?.id ?? "not placed"}` : "Choose your handle"}</Text>
    {avatar.status === "ready" && <Image source={{ uri: avatar.url }} style={{ width: 96, height: 96 }} />}
    <Text>{problem || (save.last?.state === "failed" ? save.last.why.message : save.last?.state === "pending" ? "queued" : save.last?.state)}</Text>
    <Text>{place.last?.state === "failed" ? place.last.why.message : place.last?.state}</Text>
    <TextInput placeholder="Handle" value={handle} onChangeText={setHandle} autoCapitalize="none" />
    <Button title="Save profile" disabled={busy} onPress={() => void save.run({ handle })} />
    <TextInput placeholder="Avatar image URI" value={uri} onChangeText={setUri} />
    <Button title="Place avatar" disabled={busy || !("data" in profile && profile.data)} onPress={() => void upload()} />
    <Button title="Sign out" onPress={() => void auth.logout()} />
  </View>;
}

A failed handle save keeps the session; choose another handle. Under AppProvider, online mutations persist IDs and arguments before dispatch; local devices own a durable outbox. Pending writes stay with their original account. Terminal sent confirms execution; failed carries the refusal.

Explicit write IDs must use the accepted form (26 Crockford base32 characters, starting with 0–7). Normally omit id; the client mints it. To keep one intent across retries, call mintId() once and keep that ID with its unchanged arguments. For a fixture or seed label, idFrom(label) gives the same valid ID every time (it sorts before minted IDs):

import { mintId, idFrom } from "snapback4/client";
const intent = { id: mintId(), args: { handle: "alice" } };
await client.mutate(api.saveProfile, intent.args, { id: intent.id }); // a fixture: { id: idFrom("seed:alice") }

A fresh ID on every retry does not identify that same intent. For a local send that queues without predicting a message, render its arguments from await client.pendingWrites() until it settles.

5. Export an offline shell, then verify

npm run build:web in the Expo starter runs expo export -p web followed by snapback4 web-shell dist. The Vite starter's npm run build ends with the same command. For an existing export, run npx snapback4 web-shell dist --base / and serve that directory over HTTPS (localhost also works). Every exported file is public shell content; keep exports nonpersonalized. The worker never caches API responses or assets from the backend. A replacement waits for old tabs to close.

Sign up, save a profile and view its avatar, reload, then open the acquired profile with the network off for a cold-offline start. Reconnect to settle pending writes. Browser eviction can remove offline availability; client.acquired states whether the replica is ready. Defer feeds, search, schedules and server handlers until those features are needed.

6. Add a thread without losing its history

Keep the conversation and its messages with retain delivered-history. Pin the screen by conversation ID; the inbox depends on membership and drops that row when its member row is deleted. The bare history is an ordinary query with no membership require: table rules still protect every server read. The local client asks the server before acquisition or beyond coverage, and keeps a left group's retained rows local, offline and online, through remount and reopen. An online-only client cannot recover those retained rows.

table conversations:
  title: text <=120
  creator: principal
  read <- exists members[.id, viewer]
  insert <- .creator = viewer
  after insert <- exists members[conversation = .id, principal = .creator]
  retain delivered-history
  sync to members[.id]
table members:
  conversation: conversations
  principal: principal
  unique conversation, principal
  cap 50 by conversation
  immutable conversation, principal
  read <- exists members[.conversation, viewer]
  insert <- created conversations[.conversation] or exists members[.conversation, viewer]
  delete <- .principal = viewer
  sync to members[.conversation]
table messages:
  conversation: conversations
  sender: principal
  text: text <=2000
  at: time
  search text
  immutable conversation, sender, at
  read <- exists members[.conversation, viewer]
  insert <- .sender = viewer and exists members[.conversation, viewer]
  retain delivered-history
  sync to members[.conversation] last 500 by .at
query conversationScreen(conversation: conversations):
  return conversations[conversation]
query inbox(c: cursor ?):
  return members where .principal = viewer first 50 by .conversation after c { conversation, chat: conversations[.conversation] }
query history(conversation: conversations, c: cursor ?):
  return messages where .conversation = conversation last 20 by .at after c
query search(q: text <=100, c: cursor ?):
  return messages matching q last 20 by .at after c

Render receipts, profiles, quotes and reactions separately so loading or refusal preserves history. Run npx snapback4 guide receipts for the complete model, SDK delivery reporting and screen-owned visible seen reporting. Ordinary search combines current server hits with retained device hits online; usePage accumulates by ID. Show “Partial results” only for complete: false. This windowed search emits N_SYNC_ORDER for device coverage and N_NEVER_COMPLETE for empty-needle browsing (guide search).

cap 50 by conversation is this product's maximum group size, enforced on writes. Choose caps only for actual product limits: the engine chooses its own execution bounds, and sync last 500 bounds acquisition, not how many messages may exist.

When adding Following, view following owns that name. Name its reader query home, as npx snapback4 guide feeds shows; a query also named following collides with the view.

The whole model

Edit snapback/*.q. check and dev generate snapback/generated/api.ts; never commit it. A table holds data. Rules authorize each row. Sync chooses what devices hold. A query states a predicate and order. A view derives data. A mutation commits atomically. A job sweeps; an effect records external work. An asset follows its row's read rule. check gives summaries; check --verbose adds rewrites and explanations. check --cost gives certificates: constant (bounded work at any data size), constant once background cleanup catches up, or grows with a named site. A constant verdict proves a degradation rule; timing one measures noise. Cite the verdicts in scale reports.

complete means the query answered its declared request within known coverage and execution limits. A full first N or last N is complete without a cursor. Indexes and job chunk sizes are chosen by the engine. Streaming aggregates count a domain under the meter; maintained views read stored totals. A cut total is unavailable. Jobs run as the system: select the recipient's authorized data before emitting. once prevents repeated emission; declare snapback/workers/push.mjs and opt into runWorker({ ledger: true, ... }) for durable completion deduplication; pass meta.idempotencyKey to the provider. See guide maintains, guide jobs and guide effects for recipes.

Import usePage, useMutation and useAsset from snapback4/react; use them with the generated api handles. usePage keeps loaded pages while the next loads; it supplies accumulated items, hasMore, loadMore and read completeness. For generated queries returning source rows, it keeps one newest observed image per row ID; projections keep list semantics even when they contain a field named id. Both read hooks keep the same-key answer marked revalidating through refetch, reconnect, slow reads, offline transport and retry exhaustion, with no timeout. Local commits replace it directly after the replica re-read. All change evidence—local commits, acknowledged online mutations, durable and ephemeral /changes, and /sync publications—starts a one-second bound and withdraws on failure or timeout. Successful immediate replica replacements never show loading. An older replica read cannot clear a pending notification; sync must apply its cut or a current server answer must replace the display. Offline ephemeral answers keep their original row expiry. Membership removal replaces the answer with the denial or empty result. keep: false withdraws immediately; keep: true adds a 30-second linger after the last subscriber. Key changes, deadlines, sign-out, credential, store and schema resets still withdraw. A reported program-generation change withdraws immediately, even with no changed tables; the old replica stays unavailable until adoption completes. Offline retention uses fresh: false and since; no profile identity latch is needed. useMutation tracks writes; useAsset resolves a placed asset. Upload bytes with client.upload before passing the asset id to its placement mutation. Operations in schema.q are api.name; module files use api.module.name. Effects run in declared Node entries snapback/workers/<service>.mjs: dev and serve start, restart and stop them, with service tokens injected and logs redacted. guide effects shows ledger: true and the stable meta.idempotencyKey; a crash after the provider call but before the ledger commit still needs provider idempotency.

Public feeds share an activity union: Following delivers it to each reader; Trending ranks a finite time window (npx snapback4 guide feeds).

Reference map: open only what the feature needs

snapback4 guide <card> and snapback4 why <code> answer in the terminal. Bare guide prints a two-line index of cards and aliases; guide language prints the language card. Every refusal names a code, site and rewrite. Every Quarry example compiles in the suite; the language card also runs. The chat example is optional. | Need | One reference lookup | |---|---| | Quarry grammar | guide language | | Complete chat example / offline web starter | guide app / Vite starter | | Fields / indexes / rules / aggregates | guide types / guide indexes / guide rules / guide maintains | | Reads / shapes / writes / helpers / chat receipts | guide reads / guide shapes / guide writes / guide programs / guide receipts | | Device contents / prediction / acquisition costs | guide sync / guide prediction / guide residency | | Following: view following / Global: planned view / Trending: predicate read / search | guide feeds / guide search | | Identity / media / expiry / seeds / clock | guide personas / guide assets / guide expire / guide replay / guide clock | | Typing / scoped presence | guide ephemeral / Ephemeral writes | | Background or network work (no directive) | guide jobs / guide effects (effect handlers) / guide presence / guide native / guide services (mintSession, revokeSessions) | | Limits / failures / adoption / simulator proof | guide bounds / guide refusals / guide evolution / guide witness; why <code> | | Client patterns / auth / React list | guide client / Client §4 / React list | | Development and wire | §5 | | Release | §6 | | Expo | §7 | | Exclusions | §8 | | Actions, routes and action-bound identity (use server) | guide actions / guide routes / SERVER.md (OIDC bound-session recovery is protocol-double-only; repaired-engine qualification is pending) |