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

@sveltebase/sync

v1.7.2

Published

Reactive, local-first database synchronization for Svelte 5 on Cloudflare Workers.

Readme

@sveltebase/sync

Reactive, local-first database synchronization for Svelte 5 on Cloudflare Workers.

Architecture

@sveltebase/sync uses one Cloudflare Worker:

browser
  -> SvelteKit app Worker
      /api/sync -> SyncEngine Durable Object
      server writes -> configured SyncEngine binding

The production Worker wraps the official @sveltejs/adapter-cloudflare output and exports the SyncEngine Durable Object. Vite dev uses syncDevPlugin() for the WebSocket broker and adapter-cloudflare platform proxy for local Cloudflare bindings.

Imports

import { SyncClient, createLiveQuery, createSyncClient } from "@sveltebase/sync/client";
import { defineSync, publishChangeEvent, publishEvent } from "@sveltebase/sync/server";
import { syncEngineRoute } from "@sveltebase/sync/sveltekit";
import { createSyncAppWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
import { syncDevPlugin } from "@sveltebase/sync/vite";

Client

// src/lib/sync-client.ts
import { SyncClient } from "@sveltebase/sync/client";

export const sync = new SyncClient({
  name: "app-sync",
  url: "/api/sync",
  tables: {
    todos: {
      indexes: "id, completed, updatedAt",
      channel: "todos",
      updatedAtField: "updatedAt",
    },
  },
});

updatedAtField should contain a numeric UTC timestamp such as Date.now() or new Date().getTime(). The client sends that value as since during delta sync and uses it for last-write-wins conflict handling.

For dynamic channels, derive the client options from app context and set that context from a Svelte getter. The inner WebSocket client is recreated only when the resolved context actually changes.

// src/lib/sync-client.svelte.ts
import { createSyncClient } from "@sveltebase/sync/client";

type SyncContext = { orgId: string };

export const sync = createSyncClient<AppDatabaseSchema, SyncContext>((ctx) => ({
  name: `app-sync-${ctx.orgId}`,
  url: "/api/sync",
  tables: {
    todos: {
      indexes: "id, completed, updatedAt",
      channel: `org:${ctx.orgId}:todos`,
      updatedAtField: "updatedAt",
    },
  },
}));
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { sync } from "$lib/sync-client.svelte";

  let { data } = $props();

  sync.setContext(() => ({ orgId: data.org.id }));
</script>

When using createLiveQuery with a dynamic sync client, include sync.client as a dependency so the query resubscribes after context changes:

const todos = createLiveQuery(
  () => sync.todos.toArray(),
  () => [sync.client],
);

Serialized errors

Server handlers can throw SerializableError subclasses. The sync transport sends only code and message, so the application can keep any additional formatting inside the message.

// src/lib/shared/errors.ts
import { SerializableError } from "@sveltebase/sync";

export class TranslatedError extends SerializableError {
  static readonly code = "TranslatedError";

  constructor(message: string) {
    super(message);
  }
}

Register error classes on the client to restore their prototypes after a server rejection. Unknown error codes become SerializableError instances.

import { SyncClient } from "@sveltebase/sync/client";
import { TranslatedError } from "$lib/shared/errors";

export const db = new SyncClient({
  name: "app-sync",
  url: "/api/sync",
  errorClasses: [TranslatedError],
  tables: {
    todos: {
      indexes: "id, completed, updatedAt",
      channel: "todos",
    },
  },
});
try {
  await db.todos.update(todoId, changes);
} catch (error) {
  if (error instanceof TranslatedError) {
    toast(i18n(error.message));
  }
}

Sync Handlers

Handlers run in the sync engine. Use ctx.platform.env for Cloudflare bindings, ctx.auth for verified auth data, and ctx.identity for ownership/scoped fanout.

// src/lib/server/sync-handlers.ts
import { defineSync } from "@sveltebase/sync/server";

export const todoSync = defineSync({
  channel: "todos",

  fetch: async (ctx, since) => {
    const db = ctx.platform.env.DB;
    // `since` is a UTC millisecond timestamp when the client has local data.
    return [];
  },

  authorize: async (ctx) => {
    if (!ctx.auth) throw new Error("Unauthorized");
  },

  scope: (ctx) => {
    return ctx.identity ? [ctx.identity] : [];
  },
});

export const handlers = [todoSync];

SvelteKit Route For Vite Dev

// src/routes/api/sync/+server.ts
import { sessionCookieAuth } from "@sveltebase/auth/sync";
import { syncEngineRoute } from "@sveltebase/sync/sveltekit";
import { handlers } from "$lib/server/sync-handlers";

export const { GET } = syncEngineRoute({
  handlers,
  auth: sessionCookieAuth(),
  allowUnauthenticated: true,
  // Defaults to "SYNC_ENGINE".
  syncEngineBinding: "SYNC_ENGINE",
});

Worker Wrapper For Wrangler And Production

// src/worker/app.ts
import app from "../../.svelte-kit/cloudflare/_worker.js";
import { sessionCookieAuth } from "@sveltebase/auth/sync";
import { createSyncAppWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
import { handlers } from "$lib/server/sync-handlers";

export default createSyncAppWorker(app, {
  handlers,
  auth: sessionCookieAuth(),
  allowUnauthenticated: true,
  // Defaults to "SYNC_ENGINE".
  syncEngineBinding: "SYNC_ENGINE",
});

export { SyncEngine };

createSyncAppWorker() handles GET /api/sync and internal broadcast routes, then delegates all other requests to the adapter output.

Publishing Server Events

Publishing targets the current one-worker sync runtime automatically. In production, createSyncAppWorker() or syncEngineRoute() registers the configured Durable Object binding; in Vite dev, syncDevPlugin() provides the in-process broker.

import {
  publishBulkEvent,
  publishChangeEvent,
  publishEvent,
} from "@sveltebase/sync/server";

await publishEvent("todos", "update", todo.id, todo);
await publishBulkEvent("todos", [
  { action: "update", key: todo.id, data: todo },
]);

// Notify subscribed clients that the channel changed. Clients refetch the
// channel delta through the handler fetch function, so authorization stays
// centralized in fetch instead of the WebSocket payload.
await publishChangeEvent("todos");

Vite Dev

// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { syncDevPlugin } from "@sveltebase/sync/vite";
import { sessionCookieAuth } from "@sveltebase/auth/sync";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    syncDevPlugin({
      auth: sessionCookieAuth(),
      allowUnauthenticated: true,
      wranglerConfigPath: "wrangler.local.jsonc",
    }),
    sveltekit(),
  ],
});

Svelte Config

// svelte.config.js
import adapter from "@sveltejs/adapter-cloudflare";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      platformProxy: {
        configPath: "wrangler.local.jsonc",
      },
    }),
  },
};

Wrangler Configuration

Main config for Wrangler dev with remote bindings and production deploy:

// wrangler.jsonc
{
  "name": "my-app",
  "main": "src/worker/app.ts",
  "compatibility_date": "2026-06-07",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-app",
      "database_id": "..."
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "SYNC_ENGINE",
        "class_name": "SyncEngine"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["SyncEngine"]
    }
  ]
}

If your Durable Object binding uses a different name, pass the same name to the sync setup:

export default createSyncAppWorker(app, {
  handlers,
  syncEngineBinding: "MY_SYNC_ENGINE",
});
{
  "durable_objects": {
    "bindings": [
      {
        "name": "MY_SYNC_ENGINE",
        "class_name": "SyncEngine"
      }
    ]
  }
}

Local config for adapter platform proxy in Vite dev:

// wrangler.local.jsonc
{
  "name": "my-app-local",
  "main": ".svelte-kit/cloudflare/_worker.js",
  "compatibility_date": "2026-06-07",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-app",
      "database_id": "..."
    }
  ]
}

Use wrangler secret put JWT_SECRET --config wrangler.jsonc for remote/prod secrets. Use .env for Vite dev secrets loaded by the platform proxy.