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

@dropinnodex/server

v0.13.0

Published

Server-side SDK for dropin-activity — mint HS256 user tokens offline, and manage users, feeds, and webhooks from your backend. Node only.

Downloads

495

Readme

@dropinnodex/server

Server-side SDK for dropin-activity — a GetStream-shaped activity feed as a service. Node only (imports node:crypto; never ship it to a browser).

Your backend uses this to mint short-lived user tokens offline (zero network calls to the feed service), post activities, upsert users, and manage webhook destinations.

Posting from the backend is a first-class path, not a fallback: activities emitted from a database trigger or a job queue never touch a browser, and a server token can set actor and post historical time values a user token cannot.

npm i @dropinnodex/server

Usage

import { DropInServer } from '@dropinnodex/server'

const dropin = new DropInServer({
  tenantId: 'acme',
  apiKey: process.env.DROPIN_API_KEY!,       // public
  apiSecret: process.env.DROPIN_API_SECRET!, // never leaves your server
})

// Mint a token for your frontend (HS256, signed with your apiSecret). Zero calls to us.
const token = await dropin.createUserToken('user-123', { expiresIn: '1h' })

// Users — do this BEFORE posting as an actor, or their cards render with no name.
await dropin.upsertUser({ id: 'user-123', custom: { name: 'Ada', image: 'https://…' } })
await dropin.revokeUserTokens('user-123')   // log this user out everywhere

// Activities, straight from your backend
const activity = await dropin.feed('user', 'user-123').addActivity({
  verb: 'workout',
  object: 'workout:1234',
  foreign_id: 'workout:1234',              // with `time`, makes a retry idempotent
  time: new Date().toISOString(),
  custom: { sport: 'run', durationMin: 45 },
})
activity.warnings // ['actor_user_unresolved'] if that actor was never upserted

// Outbound webhooks (server-token only)
await dropin.webhooks.create({ url: 'https://acme.com/dropin-hooks' })
await dropin.webhooks.list()
await dropin.webhooks.remove(destinationId)

apiSecret is the HMAC signing key — keep it server-side. Tokens carry aud = tenantId and are capped at a 24h TTL.

url defaults to https://api.getnodex.cloud. Override it for staging, a proxy, or local development — url: 'http://localhost:3000'. No trailing slash.

webhooks.create/list return a WebhookDestination: id (pass it to remove), config.url, and credentials (the HMAC-SHA256 signing secret to verify deliveries with). Delivery infrastructure owns the rest of the shape, so extra fields stay readable rather than being typed away.

Keeping feed data fresh: objects and activity patches

Two ways to update feed data after it's posted — see the Keeping feed data fresh guide for the full rule (custom if it's true forever, an object if it changes) and why delete-and-repost is the wrong tool.

// Objects: data many activities can point at. One write refreshes every
// timeline holding a ref — no re-fan-out.
await dropin.objects.upsert('session', '1234', { spots_left: 12 }) // replace, creates if absent
await dropin.objects.patch('session', '1234', { set: { 'custom.spots_left': 11 } }) // merge, 404s if absent
await dropin.objects.get('session', '1234')
await dropin.objects.remove('session', '1234')
await dropin.batch.objects([{ type: 'session', id: '5678', custom: { spots_left: 4 } }])

// Point an activity at an object with `refs` (max 4, `type:id`):
await dropin.feed('user', 'alice').addActivity({
  verb: 'post', object: 'session:1234',
  custom: { title: 'Thursday 5-a-side' },
  refs: ['session:1234'],
})

// Patch ONE activity's own `custom` — a typo, a corrected caption.
await dropin.activities.patch(activityId, { set: { 'custom.text': 'Corrected caption' } })

// refs is patchable too — the backfill path for an activity posted before objects
// existed. It's a top-level field (not `custom.`-dotted) and replaces wholesale;
// `refs: []` clears every ref. Activity patch only — objects ignore it.
await dropin.activities.patch(activityId, { refs: ['session:1234'] })

custom is required on upsert/batch.objects — it replaces the object's custom wholesale, so omitting it would wipe the object; the server rejects the call instead. Every set/unset path (objects and activities) must start with custom.; unset is applied after set.

Errors

Every failed request rejects with a DropInApiError — the same class @dropinnodex/client throws, so one catch covers both SDKs:

import { DropInServer, DropInApiError } from '@dropinnodex/server'

try {
  await dropin.feed('user', 'user-123').addActivity({ verb: 'post', object: 'w:1' })
} catch (err) {
  if (!(err instanceof DropInApiError)) throw err   // network failure or abort

  err.code               // 'RATE_LIMITED' | 'VALIDATION_FAILED' | … — branch on this
  err.status             // 429
  err.requestId          // log it; it identifies the request in support
  err.retryAfterSeconds  // set on a 429, undefined otherwise
  err.fields             // [{ path, message }] on a VALIDATION_FAILED with detail
  err.url                // which request failed — worth logging from a multi-route job
}

RATE_LIMITED and INTERNAL are the retryable codes; everything else will fail identically on a replay. Full table: Errors.

Upgrading from 0.10.x? Before 0.11.0 this SDK threw a bare Error with the response body stringified into .message, so classifying a failure meant parsing that string. Any err.message regex you wrote — /failed: 404/, /RATE_LIMITED/ — now matches nothing, silently: no exception, just a branch that never runs again, so a self-heal stops healing and a retry stops retrying. Switch to err.status / err.code. A dual form (err instanceof DropInApiError ? err.status === 404 : /\bfailed: 404\b/.test(err.message)) lands safely before the bump — importing DropInApiError from @dropinnodex/client until then, since this package only started re-exporting it in 0.11.0.

Cancellation

Every method takes an optional RequestOptions as its last argument, for cancelling a request you no longer need (a superseded SSR render, a closed connection):

const ctrl = new AbortController()
const p = dropin.feed('timeline', 'user-123').get({ limit: 20 }, { signal: ctrl.signal })
ctrl.abort()

An aborted call rejects with what fetch throws (name === 'AbortError'), or the reason you passed to abort(). Aborting before the request goes out also skips signing a token. Token minting itself is local and synchronous — there is nothing there to cancel.

License

MIT