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

@doync/server

v0.4.0

Published

doync DoyncOrigin/DoyncMirror Durable Object base classes

Downloads

1,145

Readme

@doync/server

Durable Object base classes and Worker routing for the doync server side. Subclass DoyncOrigin (single-writer authoritative database) and DoyncMirror (read copy + client websocket terminator), point them at the same shared definition module from @doync/core, and mount routeClientUpgrade / routeAdmin in a Cloudflare Worker fetch handler.

The package ships four entries:

| Specifier | Contents | | --- | --- | | @doync/server | Origin / Mirror bases, their configs + sealing types, client-upgrade routing, consumer-visible error classes | | @doync/server/auth | Auth resolve seam (JWT / session / admission) and the types that seal DoyncMirrorConfig.auth | | @doync/server/admin | routeAdmin + Admin*/status/pool/report types that seal its responses | | @doync/server/internal | Knobs, registration plumbing, WAE constants — may break in any release |

Schema / query / mutation authoring lives in @doync/core — this package assumes those definitions already exist.

Install

pnpm add @doync/server @doync/core

Peer runtime: a Cloudflare Worker with Durable Objects (SQLite storage) and, for Mirror bootstrap / Snapshot dumps, an R2 bucket. wrangler is the usual dev/deploy tool.

End-to-end shape

Browser / RN client ──WebSocket──▶ Worker (routeClientUpgrade)
                                        │
                        resolveAssignment │
                                        ▼
                                     Origin  ──push Changelog──▶  Mirror pool
                                        │                           │
                                   run mutations             resolve queries
                                   dump Snapshots ◀── R2 ──▶ bootstrap

One Database name spans the vertical: the Origin key, the Mirror pool for that Origin, and the schema / queries / mutations / client Replica belonging to it. Use the same shared definition module on both DOs and every client.

1. Shared definitions

Author schema / queries / mutations once in a shared module — see @doync/core. The Worker imports that module the same way web and mobile clients do. The Origin/Mirror configs below import schema, queries, and mutations from that module.

2. Subclass Origin and Mirror

// worker/durable-objects.ts
import { defineAuth, DoyncMirror, DoyncOrigin } from '@doync/server'
import type { AuthConfig } from '@doync/server/auth'
import { ctxValidationSchema, mutations, queries, schema } from '../shared/data'

/** Sticky name for the single Origin that owns this database. */
export const ORIGIN_NAME = 'tasks'

export class TasksOrigin extends DoyncOrigin<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env, {
      schema,
      // Pool member stub by deterministic name.
      mirrors: (name) => env.MIRROR.get(env.MIRROR.idFromName(name)),
      // Forwarded client pushes resolve here — authorization runs once.
      mutations,
      // Snapshots every Mirror bootstraps from.
      snapshots: env.SNAPSHOTS,
    })
  }
}

export class TasksMirror extends DoyncMirror<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env, {
      schema,
      origin: () => env.ORIGIN.get(env.ORIGIN.idFromName(ORIGIN_NAME)),
      snapshots: env.SNAPSHOTS,
      // Subscribed { name, args } resolve under the verified ctx.
      queries,
      auth: defineAuth({
        jwt: { secret: env.AUTH_SECRET },
        ctxValidationSchema,
        toContext: (identity) => ({
          userId: identity.userId,
          role: String(identity.claims?.role ?? 'user'),
        }),
        // Per-operation admission — catches identities revoked after connect.
        // validate: (op) => …,
      }),
    })
  }
}

DoyncOriginConfig (required + common)

| Field | Required | Notes | | --- | --- | --- | | schema | yes | Consumer DoyncSchema (from createDoync / createDoyncDrizzle). | | mirrors | yes | (name) => MirrorEndpoint — typically env.MIRROR.get(env.MIRROR.idFromName(name)). | | mutations | no* | MutationTree from defineMutations(…) — same object clients pass. Origin flattens + ports internally. Absent ⇒ every forwarded push is rejected. | | snapshots | no* | R2 SnapshotBucket. Absent ⇒ feed still works; dumpToR2 / Mirror bootstrap throw. | | maxActiveClientsPerMirror | no | Capacity preference before pool growth (default 200). | | clientStateLifetimeMs | no | Assignment / capacity clock (default 7d). Same name and default as Mirror; set both to the same value — the engine does not RPC-sync them. | | mutationTimeoutMs | no | Force-rollback bound for a hung async mutation body (default 30s). | | snapshotEntriesThreshold | no | Changelog entries or Commits since the newest Snapshot before a refresh dump arms (default 1000). | | analyticsEngine | no | Optional Workers Analytics Engine dataset (env.ANALYTICS). Absent ⇒ zero WAE writes, zero cost. Present ⇒ Origin heartbeats + event points. | | analyticsHeartbeatMs | no | Heartbeat cadence when analyticsEngine is bound (default 60s). 0 disables heartbeats; event points still fire. Silent while the Origin is idle (no new wake source). |

*Required for a real app that accepts writes and boots Mirrors.

Workers Analytics Engine (optional)

Hand the Origin a WAE binding and it writes two kinds of points, both indexed on the Database name (originKeyOf — the same handle R2 snapshot keys under snapshots/<originKey>/… use):

  • Heartbeat (blobs[0] = 'heartbeat') — gauge snapshot riding the existing push tick and Origin alarm (no new wake source) at analyticsHeartbeatMs cadence. Doubles: headCommitV, gcFloor, changelogEntries, poolSize, maxWatermarkLag, mirrorsAhead (ack > head count — the PITR-rewind signal lag alone cannot express), activeClients. blobs[1] is the per-Mirror pack name:ack:lag:ahead,…. Suppressed entirely while idle.
  • Event points (blobs[0] = 'event', blobs[1] = kind) — written at occurrence for deployment_skew, snapshot_reap_failure, below_gc_floor, dump_chain_restart.

The point schema (names, blobs, doubles, indexes) is a documented contract — see the operations runbook. Dashboards and alert rules are the consumer's side of the line (Cloudflare's OTel export remains the path for non-WAE stacks). Kind/blob constants live under @doync/server/internal for engine siblings and tests — application code should treat the runbook as the source of truth, not those imports.

// wrangler.jsonc
{
  "analytics_engine_datasets": [
    { "binding": "ANALYTICS", "dataset": "doync" },
  ],
}

// durable-objects.ts
super(ctx, env, {
  schema,
  mirrors: (name) => env.MIRROR.get(env.MIRROR.idFromName(name)),
  snapshots: env.SNAPSHOTS,
  analyticsEngine: env.ANALYTICS, // omit entirely for zero cost
  // analyticsHeartbeatMs: 60_000, // default
})

Extend the class for domain RPCs that must run on the Origin (OAuth user upserts, bulk seed, operator triggers). Use this.runMutation((tx) => { tx.exec(…) }) so writes enter the Changelog and replicate. DO-local bookkeeping tables that must not sync are created with this.sql.exec(…) after super() and are never declared in schema.tables.

DoyncMirrorConfig (required + common)

| Field | Required | Notes | | --- | --- | --- | | schema | yes | Same DoyncSchema as the Origin. | | origin | yes | () => OriginEndpoint thunk — catch-up / bootstrap only; no standing connection. | | queries | no* | QueryTree from defineQueries(…) — same object clients pass. Absent ⇒ subscribe is a framed registration error. | | snapshots | no* | Same R2 bucket the Origin dumps to. | | auth | no | AuthConfig from @doync/server/auth (below). Absent ⇒ every connection is anonymous. | | queryTtlMs | no | Connected-clock Subscription grace after unmount (default 30m). | | maxQueryTtlMs | no | Connected-clock TTL ceiling (default 24h). | | clientStateLifetimeMs | no | Disconnected CVR reap clock (default 7d). Same name and default as Origin; set both to the same value — the engine does not RPC-sync them. Connected clients are never reaped. |

Mirrors apply state and never enforce: constraints, triggers, and defaults run on the Origin; their effects arrive as delivered row images. A Mirror never executes client-supplied SQL.

3. Wrangler bindings sketch

// wrangler.jsonc
{
  "name": "my-app",
  "main": "./worker/index.ts",
  "compatibility_date": "2025-08-03",
  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {
    "bindings": [
      { "name": "ORIGIN", "class_name": "TasksOrigin" },
      { "name": "MIRROR", "class_name": "TasksMirror" },
    ],
  },
  "exports": {
    "TasksOrigin": {
      "type": "durable-object",
      "storage": "sqlite",
      "state": "created",
    },
    "TasksMirror": {
      "type": "durable-object",
      "storage": "sqlite",
      "state": "created",
    },
  },

  "r2_buckets": [
    {
      "binding": "SNAPSHOTS",
      "bucket_name": "my-app-snapshots",
    },
  ],

  // Secrets (AUTH_SECRET, ADMIN_TOKEN, …) ride `.dev.vars` / `wrangler secret`.
  // NEVER put the same name under `vars` — it clobbers the secret on deploy.
}

Re-export the DO classes from the Worker entry so wrangler can bind them:

// worker/index.ts
export { TasksMirror, TasksOrigin } from './durable-objects'

4. Worker fetch — route upgrades and admin

import { routeClientUpgrade } from '@doync/server'
import { routeAdmin } from '@doync/server/admin'
import { ORIGIN_NAME } from './durable-objects'

export { TasksMirror, TasksOrigin } from './durable-objects'

export default {
  async fetch(request, env): Promise<Response> {
    const url = new URL(request.url)

    // Client sync WebSocket → assigned Mirror.
    // URL must include ?clientId=<id>.
    if (request.headers.get('Upgrade') === 'websocket') {
      return routeClientUpgrade(request, {
        origin: env.ORIGIN.get(env.ORIGIN.idFromName(ORIGIN_NAME)),
        mirror: (name) => env.MIRROR.get(env.MIRROR.idFromName(name)),
      })
    }

    // Operator admin surface. Mount on a path DISTINCT from the
    // client connect route. Fail-closed when ADMIN_TOKEN is empty / unset.
    // Dispatches by subpath: GET /admin/status, restore / undo-restore.
    // Token rides Authorization: Bearer <token> (or the bare token).
    if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
      return routeAdmin(request, {
        origin: env.ORIGIN.get(env.ORIGIN.idFromName(ORIGIN_NAME)),
        token: env.ADMIN_TOKEN ?? '',
        // basePath defaults to '/admin'
      })
    }

    return new Response('Not Found', { status: 404 })
  },
} satisfies ExportedHandler<Env>

routeClientUpgrade(request, endpoints)@doync/server

  • Expects a WebSocket upgrade + clientId query param.
  • One Origin RPC (resolveAssignment) picks / creates the Client’s sticky Mirror Assignment, then forwards the upgrade to that Mirror.
  • Returns 426 for a non-upgrade request, 400 when clientId is missing.
  • A lapsed Client’s rebootstrap signal rides the forwarded URL as rebootstrap=1.

routeAdmin(request, options)@doync/server/admin

  • Token-gated admin router. Options: origin, token, optional basePath (default /admin).
  • Auth is fixed Authorization bearer (or bare token on that header) — not configurable.
  • Dispatches by subpath under basePath:
    • GET <base>/statusAdminStatus JSON (pool members, watermark lag, active clients, Snapshot / GC signals, …).
    • POST <base>/restore — body { atMs } or { bookmark }.
    • POST <base>/undo-restore — empty body; uses the fence on adminStatus.
  • Token compared with constant-time equality; empty configured token denies all requests. Unknown subpaths (and the mount root) are 404 once authorized.
  • Never mount this on the ordinary client connect path.
  • Types: AdminRouteOptions, AdminStatusProvider, AdminStatus, AdminHealth, AdminOriginStatus, AdminPoolStatus, AdminMemberStatus, DeploymentSkew, plus the Origin/Mirror/Pool/GC report types (OriginStatus, MirrorStatus, MirrorPhase, BootstrapStarted, MirrorLiveReport, PoolStatus, PoolMember*, GcStatus, RegisteredSnapshot, AssignmentKind, AssignmentResolution).

5. Auth surface — @doync/server/auth

Auth resolves on the Mirror at connect / updateAuth. Order is token-first → session → anonymous. Identity is server-resolved from a verified token or session — never asserted on the wire. The raw token is verified and never retained — only the projected ctx is pinned to the socket.

import type {
  AdmissionCheck,
  AuthConfig,
  AuthEnvelope,
  AuthOperation,
  Hs256JwtConfig,
  JwksJwtConfig,
  JwtConfig,
  PublicKeyJwtConfig,
  SessionResolver,
  ToContext,
  VerifiedIdentity,
} from '@doync/server/auth'
import {
  ExpiredAuthError,
  InvalidAuthContextError,
  InvalidTokenError,
} from '@doync/server/auth'

AuthConfig

| Field | Role | | --- | --- | | jwt? | JwtConfig — HS256 shared secret, JWKS (RS256 / ES256), or pinned public key. | | session? | SessionResolver — cookie string (or null) → VerifiedIdentity \| null. | | toContext? | (identity: VerifiedIdentity) => TAuthContext — app-facing projection queries and mutation bodies read. Invoked only for a non-null userId. Default projects the bare userId. Engine lifecycle fields (expiresAt, issuedAt) stay out of ctx. | | validate? | AdmissionCheck — per subscribe / mutate gate on the Mirror. Cheap deny; authorization that mutates state still runs authoritatively at the Origin. |

JWT variants (JwtConfig)

Product fields only:

| Variant | Fields | | ------------------------ | -------------------- | | HS256 | { secret } | | JWKS (RS256 / ES256) | { jwks: { url } } | | Pinned public key | { publicKey, alg } |

// HS256 shared secret (symmetric).
const hs: Hs256JwtConfig = { secret: env.AUTH_SECRET }

// External IdP JWKS (Auth0 / Clerk / OIDC) — RS256 or ES256.
const jwks: JwksJwtConfig = {
  jwks: { url: 'https://your-idp/.well-known/jwks.json' },
}

// Pinned public JWK.
const pinned: PublicKeyJwtConfig = {
  publicKey: { kty: 'RSA' /* … */ },
  alg: 'RS256',
}

Verification uses WebCrypto only (no JWT library required on the server) and runs inside the Mirror — configure jwt / session and catch the errors: InvalidTokenError, InvalidAuthContextError, ExpiredAuthError. JWKS cache bounds and fetch/clock injects are engine defaults — lab doubles live on @doync/server/internal (JwksJwtConfigForTest), not the public type. A verified token without sub resolves anonymous — toContext is not invoked.

Session (cookie) path

const session: SessionResolver = async (cookie) => {
  const identity = await lookupSession(cookie)
  if (identity === null) return null
  return {
    userId: identity.userId,
    claims: { role: identity.role },
    expiresAt: identity.expiresAtMs, // optional
  } satisfies VerifiedIdentity
}

The session resolver's returned userId is authoritative for that path — identity is server-resolved, never client-asserted.

6. Related main-entry helpers and types — @doync/server

| Symbol | Use | | --- | --- | | Snapshot: SnapshotBucket, SnapshotManifest, SnapshotGoneError | Typing when wrapping the R2 bucket (seals config.snapshots) or reading dumpToR2() metadata. | | Routing: routeClientUpgrade, AssignmentResolver, MirrorUpgradeTarget, PoolEndpoints | Client WebSocket upgrade path. | | Endpoint / mutation path: MirrorEndpoint, OriginEndpoint, MutationResult, MutationOptions, PushOutcome | Config fields and authoritative run / push typing; a body's sql is core's MutationExec. | | Errors: MutationTimeoutError, SchemaSkewError, SnapshotGoneError | Consumer-visible failure kinds (timeout on hung bodies; schema-mismatch on feed refuse; R2 gone). |

Definitions live in @doync/core

Do not re-author schema / queries / mutations inside the Worker. Import the shared module and pass the same trees into every surface:

import { DoyncMirror, DoyncOrigin } from '@doync/server'
import { mutations, queries, schema } from '@app/shared'

Client packages (@doync/web, @doync/mobile, @doync/react) and the server DOs all take the trees (queries, mutations) directly.

Internal surface

The main entry (. / @doync/server), plus ./auth and ./admin, are the semver-governed public API this README documents.

Anything imported from @doync/server/internal may change in any release, including patches, with no notice. Sibling @doync/* packages use that subpath; application code should not.