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

@dylanmerigaud/microsaas-kit

v0.12.1

Published

The invariant machinery of the micro-SaaS template, imported (never copied) by the template and every bet: logger, env factory, oRPC/db/stripe/email seams, structured-data, shadcn ui primitives, and the Claude Code CLI structured-output transport.

Readme

@dylanmerigaud/microsaas-kit

The invariant machinery of the micro-SaaS template, imported (never copied) by the template and every bet as a versioned dependency (the pre-eject react-scripts model). A bet keeps only bet-specific code; shared machinery lives here once. No more copy divergence.

What's in it (subpath exports)

Each subpath is imported directly, e.g. import { cn } from "@dylanmerigaud/microsaas-kit/utils".

| Subpath | What it is | | ------------------ | ----------------------------------------------------------------------------------------------- | | /logger | createLogger(isDev) -> the log object (the sanctioned console user) | | /utils | cn (clsx + tailwind-merge) | | /email | sendPaymentConfirmation, createEmailDeps({ apiKey, from }), the receipt template | | /site-url | resolveSiteOrigin(configuredUrl) | | /stripe | createGetStripe(secretKey) -> memoized getStripe, talking the pinned STRIPE_API_VERSION. Unpinned, the client speaks whatever the installed SDK defaults to, so a minor bump under the ^22 peer range moves the money path's wire version with no diff to review | | /stripe-events | handleStripeEvent(event, deps, warn) + its types (the webhook money path) | | /stripe-doctor | runStripeDoctor(config, io?) -> a StripeDoctorReport (six read-only checks that the money path is WIRED: the deployed build's origin, the price mode against the key mode, charges_enabled, the webhook endpoint's subscribed events, the deployed signing secret, and the enabled payment methods against what the handler can settle). No bin on purpose: a binary in node_modules/.bin cannot reach the bet's @/lib/stripe-events, so it would have to re-derive HANDLED_STRIPE_EVENTS and reintroduce the desync that constant prevents. The bet passes its own set in, and keeps the env reading, the printing and the process.exit | | /structured-data | softwareApplicationJsonLd(input), faqPageJsonLd(entries), websiteJsonLd(input) (Google's site-name signal), organizationJsonLd(input) (a stable @id, an absolute logo, sameAs), serializeJsonLd | | /blog | BlogPost/BlogSection types, blogPostingJsonLd(input) (optional imageUrl/organizationId), newestFirst(posts), formatBlogDate(isoDate) (the typed content model + Article JSON-LD for a bet's /blog surface) | | /theme-meta | ThemeMeta type + DEFAULT_THEME_META | | /maker-handle | makerXHandle(url) | | /orpc | createContextFactory({ getSession }), createOrpcClient<Router>({ siteOrigin }) | | /db | createGetDb({ connectionString, schema }) -> memoized getDb (drizzle over Neon) | | /env | baseServerSchema, baseClientSchema, betEnvBaseOptions to spread into a bet's createEnv | | /indexnow | submitSitemap(input, fetchImpl?), submitIndexNow, buildIndexNowPayload, parseSitemapUrls (POST changed pages to Bing/IndexNow so AI search re-crawls in hours, not weeks) | | /pseo | thinEntries, assertNotThin, pageUrls, sitemapEntries on a PseoPageType (one data entry = one static page; refuses thin near-duplicate bodies and duplicate slugs, and requires a funnelEvent on every page type) | | /pins | pinTargets, renderPins(targets, io), pngDimensions, PNG_MAGIC, MIN_PIN_WIDTH (fetch each entry's non-public /pin/<category> route off a running dev server, check the PNG magic and the 1000px Pinterest minimum, write the bytes; the entry list and the output directory are parameters, and publishing stays out of the kit, a Python-plus-MCP procedure in growth-cockpit) | | /claude-cli | runClaudeCliJson, runClaudeCliVision, pickTransport, extractJsonObject, ... (subscription-billed structured output) | | /ui | shadcn primitives: Button, Card, Input, Label | | /components | ProductDemo, QueryProvider (the pricing CTA stays per-bet: it wires a bet's billing procedure) | | /analytics | Geo-scoped consent (resolveConsentGate, isConsentGated, shouldShowConsentBanner), the PostHog browser event queue (capturePostHogEvent, ...), the same-origin proxy helpers (POSTHOG_PROXY_PATH, posthogRewrites, posthogTrailingSlashRedirect), the dev-host guard (shouldCaptureFromHost, DEFAULT_SKIPPED_HOSTS), the href-to-event resolver (resolveClickEvent), trackMetaEvent, createReplayScope(prefixes, options?) (builds the fail-closed route predicate PostHogClient's isReplayAllowedPath prop expects), the AI-crawler counter (reportAiCrawlerHit, detectAiCrawler, AI_CRAWLERS: one ai_crawler_hit PostHog event per GPTBot/ClaudeBot/PerplexityBot-class request, sent from proxy.ts with a bare fetch, no person, no consent branch). Browser and Middleware safe: contains no Node built-ins | | /analytics/server | createServerAnalytics, createMetaCapi. Node-only (posthog-node, node:crypto): kept OUT of /analytics so browser and Middleware code importing that barrel never asks a bundler for node:fs | | /analytics/consent-banner | ConsentBanner. Its own subpath because it is the only analytics component needing the shadcn /ui; importing it from the components barrel forced shadcn-free sites to stub four packages | | /analytics/components | PostHogClient, MetaPixel, TrackClicks ("use client"; take site/host/the route-allowlist predicate/pixel id as props, never hardcoded), CheckoutCtaLink (a styled anchor that fires a PostHog event, checkout_click by default, tagged with a surface the bet declares its own union for) |

Three things to know before wiring analytics into a bet

One project, sliced by site. Every bet reports into ONE PostHog project (the free plan allows a single one), so every event carries site and every insight filters on it. Filter at the DASHBOARD level, not per tile: a tile filter on the same key overrides the dashboard filter rather than adding to it. extraProperties exists only to let a site mid-rename keep emitting its old property while insights migrate.

Run ingestion through your own origin. Every ad-blocker list ships the PostHog hostname by name, and on a developer audience that is 40 to 60 percent of visitors. Pass host={POSTHOG_PROXY_PATH} to PostHogClient, but next.config.ts CANNOT import the kit to get that path or its rewrites: Next transpiles next.config.ts through SWC targeting CommonJS, the kit ships ESM-only with no require condition, and both a static import and a dynamic await import() throw ERR_PACKAGE_PATH_NOT_EXPORTED there (SWC downlevels the dynamic form back into a require() call). Both shipped consumers (wedpalette, merigaud.com) inline the three rewrite rules as local literals in next.config.ts instead, catch-all last, since a catch-all placed first would swallow /static and /array:

const POSTHOG_PROXY_PATH = "/hue";
const POSTHOG_ASSET_HOST = "https://us-assets.i.posthog.com";
const POSTHOG_INGESTION_HOST = "https://us.i.posthog.com";

rewrites: () => [
  { source: `${POSTHOG_PROXY_PATH}/static/:path*`, destination: `${POSTHOG_ASSET_HOST}/static/:path*` },
  { source: `${POSTHOG_PROXY_PATH}/array/:path*`, destination: `${POSTHOG_ASSET_HOST}/array/:path*` },
  { source: `${POSTHOG_PROXY_PATH}/:path*`, destination: `${POSTHOG_INGESTION_HOST}/:path*` },
],

posthogRewrites still lives in /analytics, but not as something a config file can import: it is the executable REFERENCE for this exact shape, pinned by src/analytics/proxy.test.ts, so diff your next.config.ts literals against it by hand whenever either one changes.

proxy.ts has no such restriction: it bundles through the normal Turbopack app pipeline instead of Next's special-cased config loader, so import posthogTrailingSlashRedirect and POSTHOG_PROXY_PATH from @dylanmerigaud/microsaas-kit/analytics there normally (static or dynamic both resolve fine). next.config.ts's skipTrailingSlashRedirect: true (PostHog endpoints end in a slash) switches OFF Next's redirect for real pages, so proxy.ts must re-apply it with posthogTrailingSlashRedirect or every page answers on two URLs and splits its own SEO.

One more place the same path has to agree: proxy.ts's middleware matcher must exclude the proxy path, and Next requires that matcher to be a statically analysable array of literals, so it cannot interpolate POSTHOG_PROXY_PATH into the exclusion. Assert the literal against the constant at runtime instead, the way merigaud.com/proxy.ts does it. Skip that assertion and a drift between the two goes unnoticed: ingestion blackholes silently, with no error anywhere.

Keep uiHost at its default unless the org moves region, or the toolbar breaks.

The banner's chrome is yours if you want it. ConsentBanner renders shadcn markup by default, which needs /ui. A site without shadcn passes a children render prop and gets only the decision plus the two posthog-js consent calls. A site that wants neither can compose its own from shouldShowConsentBanner, readConsentCookie, getPostHogInstance and POSTHOG_READY_EVENT directly.

Consumption pattern (why the template keeps thin shims)

The template (and each bet) keeps THIN SHIM FILES at the existing paths that re-export or bind the kit: lib/logger.ts becomes export const log = createLogger(env.NODE_ENV !== "production"), lib/utils.ts becomes export * from "@dylanmerigaud/microsaas-kit/utils", and so on. So @/lib/logger imports keep working everywhere, the repo rules keep their enforcement anchor (only lib/logger.ts may console, only lib/env.ts touches process.env), and a bet that must diverge on one module deletes the shim and owns the file again, explicitly. That is THE divergence mechanism.

Env-bound and per-bet-typed modules (logger, site-url, stripe, email, db, orpc, structured-data, analytics's createMetaCapi/createServerAnalytics) are exported as FACTORIES the bet binds; pure modules (utils, theme-meta, maker-handle, stripe-events, ui, components, analytics's consent-geo/posthog-consent/posthog-browser/meta-pixel-client, analytics/components) are bare re-exports.

Consumer requirements

  • Tailwind v4 must scan the kit's dist. Add to the consumer's globals.css: @source "../node_modules/@dylanmerigaud/microsaas-kit/dist";
  • The kit ships peerDependencies (react, next, stripe, drizzle-orm, radix-ui, ...); the consumer already owns them, so nothing is bundled twice.

The update flow

  1. Edit the source here (packages/microsaas-kit/src/...), with its tests.
  2. pnpm build && pnpm test (both are gates; prepublishOnly runs them).
  3. Bump version, then npm publish (access is public).
  4. Bump @dylanmerigaud/microsaas-kit in each consumer (the template and every bet) and reinstall.

During development in THIS repo the template links the kit through the pnpm workspace (a file: dependency), so edits are picked up on the next pnpm build without publishing. In a bet clone the kit resolves from npm, so packages/ here is inert source there.

Build

ESM only (type: module), NodeNext, tsc to dist/ with .d.ts. The React subpaths keep their "use client" directive through the build (verified in dist output).