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

@zykeco/sync-server

v0.12.0

Published

Self-hosted Zyke sync server (Hono + Drizzle + libsql). Imports to auto-start; ships a migrate CLI.

Readme

@zykeco/sync-server

Self-hosted sync server for the Zyke mobile app. A small Hono HTTP service backed by libsql and Drizzle ORM, validated end-to-end with TypeBox schemas from @zykeco/sync-protocol.

  • Single-tenant, secret-gated. One server per deployment; two bearer secrets (READ_SECRET, WRITE_SECRET).
  • Writes + hard-deletes only. No read API surface — the mobile client is the source of truth, the server is a durable buffer for backup/restore.
  • Schemaless sleep_sessions. Domain fields live inside a JSON payload, so the mobile app evolves the shape without a server release.
  • Three CLI bins. zyke-sync-startup, zyke-sync-upgrade, and zyke-sync-migrate cover first-run setup, version bumps, and ad-hoc migrations.

Requires Node ≥ 24 and npm ≥ 11.

Install

npm install @zykeco/sync-server

For a deployment-ready starter that already wires up startup / start / upgrade, see the @zykeco/sync-self-host template — copy it onto your server and you're done.

Quick start (manual)

# 1. Create a project directory with @zykeco/sync-server installed.
mkdir my-sync && cd my-sync
npm init -y && npm pkg set type=module
npm install @zykeco/sync-server

# 2. Generate .env (READ_SECRET / WRITE_SECRET) and run migrations.
npx zyke-sync-startup

# 3. Create index.js with one line, then start.
echo "import '@zykeco/sync-server';" > index.js
node index.js
# → zyke-sync listening on http://localhost:3000

Bins

| Command | Purpose | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | zyke-sync-startup | First-run setup. Runs npm install, generates/updates .env with crypto-random READ_SECRET and WRITE_SECRET (mode 0600), creates the libsql data dir, then runs migrations. Idempotent. | | zyke-sync-upgrade | Upgrades the package and migrates. zyke-sync-upgrade --to 0.5.0 for a specific version; no arg defaults to latest. | | zyke-sync-migrate | Applies all pending Drizzle migrations against DATABASE_URL. Run by startup / upgrade; useful on its own for ad-hoc migrations. |

Environment

The server reads these via process.env (and .env if present):

| Var | Default | Purpose | | -------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | PORT | 3000 | HTTP listen port. | | DATABASE_URL | file:./data/sync.db | libsql connection string. Accepts local SQLite (file:) or remote libsql/Turso. | | READ_SECRET | required | Bearer token for read endpoints (currently unused — reads are not exposed). | | WRITE_SECRET | required | Bearer token for write + delete + wipe endpoints. | | PUBLIC_URL | unset | Public base URL, e.g. https://sync.example.com. Used as the OAuth issuer for MCP discovery + redirect URLs. Set this when running behind a TLS-terminating reverse proxy (see MCP server). |

zyke-sync-startup generates 32-byte hex values for both secrets if they're missing, writes the file with mode 0600, and leaves existing values alone on subsequent runs.

HTTP API

All write endpoints take Authorization: Bearer ${WRITE_SECRET} and respond with JSON.

Health

GET /v1/health  →  { ok: true }

Generic batch upsert

Storage is schemaless: every synced entity is a generic envelope stored in one records table keyed by (collection, id); the domain shape lives entirely in an opaque JSON payload.

POST /v1/sync/{collection}/batch

body:  { rows: RecordEnvelope[] }   // max 500
reply: { stored: string[], serverNowMs: number }

A RecordEnvelope is { id: string, updatedAt: number, payloadVersion: number, payload: object, attachment?: object | null }. Upserts by (collection, id) with a last-write-wins guard on updatedAt — older writes are silently ignored. attachment holds an optional heavy secondary blob (e.g. an activity's heart-rate samples) and is excluded from list-style MCP reads.

Known collections (anything else → 404): daily_metrics, weekly_metrics, sleep_sessions, user_timezones, user_profile, daily_stress_burden, heart_rate_minute, hr_zone_history, activities.

Generic hard-delete

POST /v1/sync/{collection}/delete

body:  { ids: string[] }        // max 500
reply: { deleted: string[], serverNowMs: number }

Missing ids are silent successes — safe to retry.

Wipe everything (destructive)

POST /v1/sync/wipe

body:  { confirm: "wipe-all-data" }
reply: { deleted: number, serverNowMs: number }

Deletes every record across all collections (blobs untouched). The literal confirm string is enforced at the protocol layer.

MCP server

The server ships an MCP endpoint at POST /mcp that exposes read-only tools over your synced data. Auth supports two OAuth 2.1 flows:

  • authorization_code + PKCE — for interactive clients like the Claude.ai web app and Claude Desktop "Custom Connectors". Dynamic Client Registration (RFC 7591) is supported.
  • client_credentials — for headless agents / CLIs that hold the READ_SECRET directly.

Required env vars

| Var | Default | Purpose | | ----------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | MCP_ENABLED | true | Set false to disable the /mcp mount entirely. | | MCP_CLIENT_ID | req. | Bootstrap client ID for the headless client_credentials grant. Public — pick anything memorable. | | MCP_TOKEN_SECRET | req. | HMAC key for signing access + refresh tokens. Must be ≥ 32 chars (enforced at startup) — generate with openssl rand -hex 32. Treat as a secret. | | MCP_TOKEN_TTL_SECONDS | 3600 | Bearer (access) token lifetime. | | MCP_REFRESH_ENABLED | true | Issue refresh tokens on the auth-code grant so clients renew without re-login. false to disable. | | MCP_REFRESH_TTL_SECONDS | 2592000 | Sliding refresh-token lifetime (30 d). Each refresh re-issues with a fresh window. | | MCP_REFRESH_MAX_TTL_SECONDS | 7776000 | Absolute cap on a refresh chain from first issue (90 d). After this, re-auth is required. |

HTTPS requirement & PUBLIC_URL

The MCP authorization-code flow (OAuth 2.1) requires HTTPS in production — claude.ai and other MCP clients refuse to follow http:// authorization endpoints. The sync server itself terminates plain HTTP; run it behind a reverse proxy (Caddy / Traefik / nginx) that terminates TLS.

Set PUBLIC_URL to your public HTTPS URL so discovery + redirect URLs are correct:

PUBLIC_URL=https://sync.example.com

When PUBLIC_URL is set, it always wins. Without it, the server falls back to X-Forwarded-Proto + X-Forwarded-Host from the proxy, then to the request URL as last resort. The fallback is fragile (depends on the proxy setting the headers correctly), so the explicit env var is recommended for any production deployment.

Endpoints

| Method | Path | Purpose | | ------ | ----------------------------------------- | ----------------------------------------------------------------------------- | | GET | /.well-known/oauth-authorization-server | RFC 8414 metadata (root-level, what MCP clients probe). | | GET | /.well-known/oauth-protected-resource | RFC 9728 resource metadata pointing back to the AS. | | POST | /mcp/register | Dynamic Client Registration (RFC 7591). | | GET | /mcp/authorize | Consent screen (PKCE flow). | | POST | /mcp/authorize | Consent form submit — issues an authorization code. | | POST | /mcp/oauth/token | Token endpoint (authorization_code, client_credentials, refresh_token). | | POST | /mcp | MCP JSON-RPC. Requires Authorization: Bearer <token>. |

Connecting Claude.ai (web or desktop) as a custom connector

  1. Deploy your server with PUBLIC_URL + MCP_* env vars set behind a TLS-terminating proxy. Confirm:
    • https://YOUR_HOST/.well-known/oauth-authorization-server returns JSON
    • The issuer field in that JSON is your https:// URL, not http://. (If it's http, set PUBLIC_URL and restart.)
  2. Open Claude.ai → Settings → Connectors → Add custom connector.
  3. Server URL: enter https://YOUR_HOST/mcp (the /mcp path matters — that's the JSON-RPC endpoint, not the host root).
  4. Click Connect. Claude.ai will:
    • Fetch the discovery doc at /.well-known/oauth-authorization-server.
    • Self-register via POST /mcp/register.
    • Open a new tab to https://YOUR_HOST/mcp/authorize?....
  5. On the consent screen: verify the "Redirects to" line shows https://claude.ai/api/mcp/auth_callback. Paste your READ_SECRET into the form and click Authorize.
  6. You're done. Claude.ai redirects back, exchanges the code for a bearer token, and the connector goes online. The read tools (query_collection, list_daily_metrics, list_sleep_sessions, list_activities, get_activity, get_user_profile, …) become available in the chat tool picker.

Claude Desktop's "Add custom connector" flow is identical — same form, same URL.

What if Claude.ai shows a 404 at /authorize?

That means it's hitting the host root instead of the discovered path. Two causes:

  • The discovery doc isn't reachable. Curl https://YOUR_HOST/.well-known/oauth-authorization-server and confirm you get JSON, not your reverse proxy's 404 page. Add the path to your proxy rules.
  • You're on @zykeco/sync-server < 0.7.0. Upgrade — the auth-code flow ships from 0.7.0 onward.

Token / client lifetime

  • Bearer (access) tokens expire after MCP_TOKEN_TTL_SECONDS (default 1 h).
  • When MCP_REFRESH_ENABLED is on (default), the auth-code grant also returns a refresh token. Clients (claude.ai, Claude Code) silently exchange it for a new access token via grant_type=refresh_token, so you no longer re-run the consent flow every time the access token expires. Refresh tokens are stateless and signed: they keep working across server restarts and slide forward on each use, up to the MCP_REFRESH_MAX_TTL_SECONDS cap. Because they are stateless, this is sliding re-issuance, not reuse-detected rotation: the refresh token you just exchanged stays valid until its own expiry (it is not invalidated by the re-issue), and individual tokens cannot be revoked before then — rotate MCP_TOKEN_SECRET to invalidate all of them at once.
  • DCR client registrations are in-memory — they don't survive a server restart. Refresh tokens do (the client binding is in the signature), so a restart no longer forces a re-login; only brand-new connections need DCR to re-run, which claude.ai does automatically.

Connecting from headless agents (client_credentials)

# 1. Get a token using your READ_SECRET as the client secret.
curl -X POST https://YOUR_HOST/mcp/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=$MCP_CLIENT_ID \
  -d client_secret=$READ_SECRET

# 2. Use the access_token to call MCP.
curl -X POST https://YOUR_HOST/mcp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The authorization_code grant additionally returns a refresh_token. To renew an access token without re-running the consent flow:

curl -X POST https://YOUR_HOST/mcp/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=$CLIENT_ID \
  -d refresh_token=$REFRESH_TOKEN
# -> { access_token, token_type, expires_in, refresh_token }  (a new refresh_token is re-issued)

(The headless client_credentials grant does not return a refresh token — it just re-requests with the static secret.)

Data model

  • daily_metrics, weekly_metrics{ id (PK), isoDate / weekStartIsoDate, metricKey, value, createdAt, updatedAt }. Composite unique on (date, metricKey).
  • sleep_sessions — schemaless envelope: { id (PK), isoDate, updatedAt, payloadVersion, payload (JSON) }.

All timestamps are epoch milliseconds. ids are client-generated integers (no autoincrement on the server).

Programmatic use

import '@zykeco/sync-server'; // auto-starts on import using process.env

There is no module API to embed the server in another process — the package is import-to-run by design.

Compatibility

| Runtime | Status | | ---------- | -------------------------- | | Node ≥ 24 | ✅ | | Bun / Deno | ❌ (libsql native binding) |

For embedded/edge runtimes, point a stock Node deployment at a remote libsql (Turso) URL.

License

AGPL-3.0-or-later — see LICENSE.