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

@the-meridian/sdk

v1.5.0

Published

Meridian SDK for Shopify embedded apps — entitlements, billing, plan/account UI, and usage tracking against the Meridian API.

Readme

@the-meridian/sdk

The runtime SDK for adding Meridian billing and entitlements to a Shopify embedded app. Mount one provider, gate features synchronously in render, render the pricing and account pages, and track usage — with a separate server entry for resource routes, extensions, and background jobs.

npm install @the-meridian/sdk

react and react-dom (>=18) are peer dependencies. The package ships ESM + CJS with types, and two entry points:

  • @the-meridian/sdk — the React surface (browser).
  • @the-meridian/sdk/server — the server surface (Node, no React).

How auth works

The SDK never sees your app's Meridian API key. The flow is:

  1. Server, once per authenticated request: exchange the shop for a per-shop shopToken (createMeridianApp().mintShopToken(...), which calls Meridian's identify). Persist the shopToken and hand it to the browser via your loader.
  2. Browser: mount <MeridianProvider shopToken={shopToken}>. Every read and billing call goes straight to Meridian authenticated with the shopToken bearer — Meridian resolves the Shopify Admin token it needs on its side, so there is no server bridge to build for subscribe/cancel/track.

Set these env vars on the server (read by createMeridianApp):

| Variable | Required | Purpose | | ------------------- | -------- | ---------------------------------------------------- | | MERIDIAN_APP_ID | yes | Your Meridian app UUID. | | MERIDIAN_API_KEY | yes | Server-to-server API key (never sent to the browser).|

Security model

The shopToken is a per-shop, short-lived bearer (it expires after ~1 hour; the provider refreshes it transparently). It is intentionally safe to hand to the browser:

  • Scope — it authorizes reads and billing writes (subscribe, cancelSubscription, updateUsageCap, track) for one shop only. It is not your MERIDIAN_API_KEY, cannot call the Shopify Admin API directly, and cannot reach any other shop's data.
  • Blast radius — because billing calls run client-side, any script on the page can use the token while it is valid; the impact is bounded to that one shop's billing.

For apps that want sensitive operations enforced on your own server instead of client-side, pass the on* overrides — the provider then calls your handler instead of Meridian directly:

<MeridianProvider
  shopToken={shopToken}
  onSubscribe={(planId, returnUrl, interval) => myServer.subscribe(planId, returnUrl, interval)}
  onCancelSubscription={() => myServer.cancel()}
  onUpdateUsageCap={(input) => myServer.updateCap(input)}
  onTrack={(input) => myServer.track(input)}
>
  {/* app shell */}
</MeridianProvider>

Point baseUrl only at an HTTPS Meridian host — the SDK warns if a non-HTTPS, non-localhost baseUrl is configured, since the token would otherwise be sent over an insecure connection.

Server setup

Create one Meridian app entrypoint. It degrades gracefully when the env vars are absent (configured === false, null clients, ungated gates) so the app still renders.

// app/meridian.server.ts
import { createMeridianApp } from "@the-meridian/sdk/server";

export const meridian = createMeridianApp();

In your Shopify afterAuth hook, wire Meridian with a single call. It fetches the server-driven webhook topic set (managed billing topics plus any developer-routed topics configured in your Meridian dashboard), registers the webhooks, and reports the authentication so install/auth automations fire — you never declare webhook topics yourself:

hooks: {
  afterAuth: async ({ session }) => {
    await meridian.afterAuth(session);
  },
},

afterAuth is best-effort and never throws: it returns a { registered, updated, errors, lifecycleNotified } summary, falls back to the embedded managed topics if the webhook config is unreachable, and always unions those managed topics in so billing webhooks are never dropped. The lower-level meridian.registerWebhooks(...) and meridian.lifecycle?.notifyAuthenticated(...) remain available if you need to call them separately.

In the loader for your embedded app shell, mint the shopToken and pass it down:

export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  const shopToken = await meridian.mintShopToken({ shop: session.shop });
  return { shopToken };
};

Mount the provider

import { MeridianProvider } from "@the-meridian/sdk";

export default function App() {
  const { shopToken } = useLoaderData<typeof loader>();
  return shopToken ? (
    <MeridianProvider shopToken={shopToken}>
      {/* app shell */}
    </MeridianProvider>
  ) : (
    /* render without Meridian features */ null
  );
}

loading stays true until the first /me fetch resolves — render skeletons or null during loading to avoid a flash. The provider transparently refreshes an expired shopToken.

Gate features

useMeridian() exposes synchronous gates for render paths. Features and usage events are addressed by their stable app-local key.

import { useMeridian } from "@the-meridian/sdk";

function AdvancedReports() {
  const { isEnabled, getLimit, getUsage } = useMeridian();
  if (!isEnabled("advanced_reports")) return <UpgradePrompt />;

  const limit = getLimit("seats");                 // number | undefined
  const used = getUsage("api_calls")?.usedThisPeriod;
  return <Reports seatLimit={limit} apiCallsUsed={used} />;
}

You also get customer, entitlements, plans, refresh(), and the write methods subscribe(), cancelSubscription(), updateUsageCap(), and track().

Track usage

const { track } = useMeridian();
await track({ eventKey: "api_calls", quantity: 1 });

Metered events appear under entitlements.events[eventKey].usedThisPeriod after the next refresh.

Pricing & account pages

Both require a <MeridianProvider> ancestor.

Both pages render Polaris web components (<s-*> elements), which the Shopify admin registers through the App Bridge script. They therefore must render inside an embedded Shopify app with App Bridge loaded — every Shopify app template (AppProvider embedded / the app-bridge.js script tag) already provides this. Outside the embedded admin the elements are unregistered and render as unstyled text (a console warning points this out in dev). Render them inside your own <s-page><s-section> so they inherit the page chrome:

<s-page heading="Pricing">
  <s-section>
    <MeridianPricingPage returnUrl={returnUrl} />
  </s-section>
</s-page>

MeridianPricingPage needs a returnUrl — where Shopify sends the merchant back after they approve (or decline) the charge. It must be an https URL on the shop's own admin, i.e. https://<shop>.myshopify.com/admin/... or https://admin.shopify.com/store/.... Anything else — your app's own tunnel/hosting origin, request.url, window.location, or any http:// URL — is rejected with INVALID_RETURN_URL, because Shopify's billing API only accepts shop-admin return URLs. Using the shop-admin URL is also what re-embeds your app after approval instead of bouncing the merchant to the login screen.

Because it depends on the session shop and your Shopify API key (both server-only), build it in the loader — never from the request URL or in the browser:

// app/routes/app.pricing.tsx
export const loader = async ({ request }) => {
  const { session } = await authenticate.admin(request);
  // https://<shop>.myshopify.com/admin/apps/<client_id>/<your-app-route>
  // session.shop is the shop's *.myshopify.com domain; SHOPIFY_API_KEY is your app's client id.
  const returnUrl = `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/pricing`;
  return { returnUrl };
};
import { MeridianPricingPage, MeridianAccountPage } from "@the-meridian/sdk";

export default function Pricing() {
  const { returnUrl } = useLoaderData<typeof loader>();
  // Pass the loader-built returnUrl straight through — do not construct it inline.
  return <MeridianPricingPage returnUrl={returnUrl} />;
}

// The account page takes no returnUrl:
<MeridianAccountPage onChangePlan={() => navigate("/app/pricing")} />

Pricing button labels react to the active plan (Subscribe / Upgrade / Downgrade / Change to this plan / Current plan). The account page shows per-event usage and a cancel button; the cancel confirmation and raise-cap flows use Polaris modals (native window.confirm / window.prompt are suppressed inside the embedded admin's iframe).

Server-side (no React)

For resource routes, Shopify extensions, and background jobs, build a server client from a shopToken:

import { createMeridianServerClient } from "@the-meridian/sdk/server";

const client = createMeridianServerClient(shopToken);
const { enabled, limit } = await client.checkFeature("advanced_reports");
await client.track({ eventKey: "api_calls", quantity: 5 });

// Entitlements + key-based gating helpers in one call (degrades to ungated on failure):
const gates = await client.getGates();
if (gates.can("advanced_reports")) { /* ... */ }

Or resolve gates straight from a shop in a loader/webhook without minting a token yourself:

const gates = await meridian.gatesForShop(session.shop);

Receiving forwarded webhooks

Meridian re-serializes Pub/Sub-sourced events and forwards them to your declared host, so Shopify's own HMAC can't validate the body — verify Meridian's X-Meridian-Signature instead. Before it starts forwarding to a new host, Meridian sends a signed verification challenge to the same path that you answer with your signing secret. Handle both in one route (default path /webhooks/shopify): try the challenge first, then fall through to the forwarded-webhook path.

import {
  meridianChallengeResponse,
  verifyForwardedWebhook,
} from "@the-meridian/sdk/server";

export const action = async ({ request }) => {
  const raw = await request.text();
  const sig = request.headers.get("X-Meridian-Signature");
  const secret = process.env.MERIDIAN_WEBHOOK_SECRET ?? "";

  // 1. Verification handshake? Answer it and stop.
  const challenge = await meridianChallengeResponse(raw, sig, secret);
  if (challenge) return Response.json(challenge);

  // 2. Otherwise it's a forwarded webhook — verify, then handle the event.
  if (!(await verifyForwardedWebhook(raw, sig, secret))) {
    return new Response("invalid signature", { status: 401 });
  }
  const topic = request.headers.get("X-Shopify-Topic");
  console.log(`Received forwarded ${topic} webhook`);
  return new Response(null, { status: 200 });
};

The signing secret is shown in your Meridian dashboard; set it as MERIDIAN_WEBHOOK_SECRET.

API reference

See docs/api.md for the full curated surface.

License

MIT