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

@findlane/server

v0.39.0

Published

Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.

Readme

@findlane/server

The Findlane server SDK: upload and update catalogs, manage index settings, and search, with a secret workspace API key. It runs wherever fetch does: Node.js 18+, Bun, Deno, and edge runtimes. Never bundle it, or its key, into browser code; use @findlane/browser there.

Docs: https://findlane.dev/docs/server-sdk

Install

npm install @findlane/server

Quick start

import { SearchServerClient } from "@findlane/server"

const client = new SearchServerClient({
  workspaceId: process.env.FINDLANE_WORKSPACE_ID!,
  apiKey: process.env.FINDLANE_API_KEY!,
})
const products = client.index("products")

await products.upsert([{ objectID: "sku-1", title: "Trail shoe", brand: { name: "Acme" }, price: 12900 }])
const results = await products.search({ q: "trail shoe", limit: 10 })
console.log(results.total, results.hits[0]?.record.title)

The workspace ID is under Settings in the dashboard, and keys are under API keys. A key has one or both scopes:

  • search: search().
  • write: record writes, settings, public search restrictions, and indexing status.

Searching for a visitor

When your server searches for someone, such as a shopper loading a server-rendered search page, pass the visitor. The index's limits and bot rules then apply to them as they do to browser search, and searches from verified crawlers like Googlebot are free:

const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent: req.get("user-agent") } })

suggest() and recommendations() take the same option. Without it, a search counts as your server's own.

An index's methods

client.index<T>(name) returns these; T types your records, which each need a string objectID.

| Method | What it does | | --- | --- | | search(request, options?) | Search with a query, filters, facets, a sort, paging, and highlighting. explain: true says why each hit matched and what ordered it, and appliedRules lists the index's query rules the query fired. options takes signal and visitor (below). | | querySuggestions({ q, limit? }), popularSearches({ limit? }) | Whole searches to offer as a visitor types, [{ q, typed }], and the index's popular searches for an empty box. Not counted as searches. | | upsert(records) | Add or update up to 1,000 records by objectID. | | replaceAll(records) | Replace the whole catalog in one request (up to 100,000 records). | | replaceAllStaged(records, options) | Replace a larger catalog in batches under one stage ID, switched in one commit. Retrying with the same stageId resumes. abortReplacement(stageId) drops a stage. | | delete(objectID) | Delete a record. | | getRecord(objectID) | A record and its version. | | upsertOne(record, { expectedVersion }), deleteOne(objectID, { expectedVersion }) | Write only if the record is still at expectedVersion (0 for one that never existed); retried with one idempotency key. | | getSettings(), replaceSettings(settings), waitForSettings() | Read and change searchable fields, facets, filters, sorts, custom ranking, synonyms, languages, typo tolerance, and text normalization. | | compareSettings(settings, { queries, searched }?) | Compare settings with the index's own without saving them: its search tests and most searched queries, each searched with both. Says which tests would fail and whose results change. | | getSearchTests(), setSearchTests(tests) | Searches that must keep working, saved apart from the settings, such as a record in the top 3 or at least 100 results. compareSettings checks them. | | getIndexingStatus(), waitUntilIndexed(revision) | Writes return an accepted revision and are searchable when accepted, so waitUntilIndexed returns at once. | | getPublicSearchAccess(), setPublicSearchPolicy(policy), usePublicIndexDefaults() | Restrict what browser search may read and query. | | events.send(events, { visitorId, visitId, abTest }?), events.importOrders(orders), events.cancelOrder(orderID) | Orders and other events for the index's Events page and Bought together; the key needs the events or write scope. Your own anonymous visitor IDs link a shopper's events, when the index takes them. abTest, from the shopper's search answer, puts them in their A/B test group. | | abTests.get(), abTests.estimate({ share }), analytics.abTest() | The index's A/B tests of its settings, with their results, and what a test would take. Owners start and end them in the dashboard. | | events.forget(ids) | Deletes the events sent with these visitor or visit IDs, within a week. | | events.proxy(request), events.relay(body, visitor) | A route on your own site for the browser SDK's events (its eventsUrl), which ad blockers let through. See Events. | | analytics.searches(), .queries({ list, limit }), .filters(), .events(), .products(), .productCounts(), .product(objectID) | The index's analytics, for your own admin: searches over time, top queries with their clicks, queries without results or clicks, the filters used, events, and each product's views, add-to-carts and orders. The key needs the analytics scope, and works only from a server. See Analytics. | | writes.get({ range }), writes.log({ problems, before, limit }) | What has come into the index: when data last arrived and last changed anything, by source, and writes refused with their reasons. The key needs the analytics scope, and works only from a server. See Updates. |

Settings

replaceSettings sends the full settings; fetch them first to keep the other fields. Its applied says how the change took effect:

  • instant: query-time settings, and removing or reordering searchable fields.
  • fields: facet, filter, sort, and ranking fields, filled in place.
  • searchable: new searchable fields, filled in.
  • rebuild: text normalization; the index is rebuilt.

A large searchable or rebuild change continues in the background, with its progress in rebuild (also returned by getSettings). Searches use the previous settings until it finishes, and waitForSettings() waits for it.

Before saving a change to rules, synonyms, typo tolerance, or ranking, compareSettings shows what it does, and {} runs the search tests with the current settings:

const comparison = await products.compareSettings({ rules: nextRules })
const failing = comparison.tests.filter((test) => test.before && !test.after)
if (!failing.length) await products.replaceSettings({ ...(await products.getSettings()), rules: nextRules })

Browser search

Only upload fields you intend to make public if browser search is enabled. getPublicSearchAccess() shows the current mode. setPublicSearchPolicy({ returnFields, searchFields, filterFields, facetFields, sortIds, defaultSort }) restricts the index at the API, and usePublicIndexDefaults() removes the restrictions. Only a workspace owner can change allowed browser origins, in the dashboard.

Signed keys

For a logged-in person's browser searches, sign a short-lived key with a signing key from the dashboard's API keys page. It makes no request, and its restrictions replace the index's public settings:

import { signSearchKey } from "@findlane/server"

const signedKey = await signSearchKey(process.env.FINDLANE_SIGNING_KEY!, {
  validUntil: new Date(Date.now() + 60 * 60 * 1000),
  indexes: ["products"],
  filters: { customerGroup: user.group },
  userToken: user.id,
})

The browser SDK takes it as signedKey. See Logged-in visitors.

Errors

Failed requests throw a FindlaneError with the HTTP status and the API's message. A 402 means a plan limit, such as records or monthly searches. replaceAllStaged throws a StagedReplacementError with the stageId to retry. Every method takes an optional AbortSignal.

License

MIT