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

better-auth-openmeter-plugin

v0.5.0

Published

Better Auth plugin for OpenMeter usage metering, entitlements, API keys, organizations, and billing providers.

Downloads

62

Readme

better-auth-openmeter-plugin

CI npm version license types

Better Auth plugin for OpenMeter usage metering, entitlements, API key tracking, organization customers, React hooks, and billing provider bridges for Stripe, Polar, Razorpay, Creem, Dodo Payments, and Autumn.

Adoption Modes

Use this package in one of two ways:

  • Primary runtime integration for greenfield Better Auth apps that want this package to own OpenMeter customer sync, billing event mirroring, entitlements, and top-up grant helpers.
  • Catalog/helper layer for mature apps that already have an OpenMeter control plane. In that mode, keep your existing provisioning/runtime logic and use this package for catalog definition, provider compilation, and audit-only billing integration.

If your app already owns org-scoped subscriptions, grant issuance, or request time accounting, prefer catalog plus entitlementMode: "none" instead of trying to replace that runtime with this plugin wholesale.

Install

npm install better-auth-openmeter-plugin @openmeter/sdk

The package supports better-auth and @better-auth/core ^1.6.3.

Server Setup

import { betterAuth } from "better-auth";
import { openmeterPlugin } from "better-auth-openmeter-plugin";

export const auth = betterAuth({
  plugins: [
    openmeterPlugin({
      apiKey: process.env.OPENMETER_API_KEY!,
      baseUrl: "https://openmeter.cloud",
      createCustomerOnSignUp: true,
      syncCustomerOnUserUpdate: true,
      trackAuthEvents: true,
      customer: {
        resolveKey: ({ user }) => user.id,
        resolveSubject: ({ user }) => user.id,
        resolveProfile: ({ user, defaults }) => ({
          ...defaults,
          description: `Better Auth user ${user.id}`,
          billingAddress: {
            country: "US",
          },
          metadata: {
            ...defaults.metadata,
            source: "better-auth",
          },
        }),
      },
    }),
  ],
});

You can also pass a preconfigured SDK client:

import { OpenMeter } from "@openmeter/sdk";
import { openmeterPlugin } from "better-auth-openmeter-plugin";

openmeterPlugin({
  openmeterClient: new OpenMeter({
    baseUrl: "http://127.0.0.1:8888",
  }),
});

Client Setup

import { createAuthClient } from "better-auth/client";
import { openmeterClientPlugin } from "better-auth-openmeter-plugin/client";

export const authClient = createAuthClient({
  plugins: [openmeterClientPlugin()],
});

What It Adds

  • openmeterCustomerId on the Better Auth user schema
  • optional openmeterCustomerId on the Better Auth organization schema
  • optional OpenMeter customer creation after Better Auth user creation
  • optional OpenMeter customer sync after Better Auth user updates
  • optional OpenMeter customer creation/sync for Better Auth organizations
  • optional auth lifecycle event ingestion
  • authenticated endpoints for user and organization usage events, customers, access, and entitlements
  • optional React Query hooks from better-auth-openmeter-plugin/react

Run your Better Auth migration/generation step after enabling the plugin so the openmeterCustomerId field exists in your database.

Customer Identity and Profile Sync

OpenMeter generates the canonical customer id. The plugin stores that value in Better Auth as openmeterCustomerId after the first successful sync.

Use resolveKey to choose the stable OpenMeter customer key. It defaults to the Better Auth user id, but you can return another deterministic value such as customer:${user.id}. Use resolveSubject to choose the OpenMeter usage subject. It defaults to the same value as resolveKey.

The plugin syncs these user customer fields by default:

  • name from user.name, user.email, then user.id
  • primaryEmail from user.email
  • usageAttribution.subjectKeys from resolveSubject
  • metadata.betterAuthUserId and metadata.betterAuthEmail
  • optional currency

Use customer.resolveProfile to customize OpenMeter customer profile fields such as description, primaryEmail, billingAddress, currency, and metadata. Profile fields are merged over the defaults; identity still comes from resolveKey and resolveSubject.

Common identity policies:

openmeterPlugin({
  openmeterClient,
  customer: {
    // Default: one OpenMeter customer per Better Auth user.
    resolveKey: ({ user }) => user.id,
    resolveSubject: ({ user }) => user.id,
  },
});

openmeterPlugin({
  openmeterClient,
  customer: {
    // Namespaced key when OpenMeter also receives customers from other systems.
    resolveKey: ({ user }) => `user:${user.id}`,
    resolveSubject: ({ user }) => `user:${user.id}`,
  },
});

Use the OpenMeter generated id only as the stored openmeterCustomerId. Prefer a deterministic key for lookups and integration events so sync can be replayed safely.

Organization Support

Organization support is optional. Enable it only when you also use Better Auth's organization plugin.

import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { openmeterPlugin } from "better-auth-openmeter-plugin";

export const auth = betterAuth({
  plugins: [
    organization(),
    openmeterPlugin({
      apiKey: process.env.OPENMETER_API_KEY!,
      organization: {
        enabled: true,
        allowedRoles: ["owner", "admin"],
        createCustomerOnOrganizationCreate: true,
        syncCustomerOnOrganizationUpdate: true,
        resolveKey: ({ organization }) => organization.id,
        resolveSubject: ({ organization }) => organization.id,
        resolveProfile: ({ organization, defaults }) => ({
          ...defaults,
          description: `Workspace ${organization.slug}`,
          metadata: {
            ...defaults.metadata,
            workspaceSlug: organization.slug,
          },
        }),
      },
    }),
  ],
});

When organization.enabled is true, the plugin checks that the Better Auth organization plugin is installed. Organization endpoints require organizationId and use Better Auth's organization-role middleware. Organization customer sync defaults to name, usageAttribution.subjectKeys, metadata.betterAuthOrganizationId, metadata.betterAuthOrganizationSlug, and optional currency; use organization.resolveProfile for the rest of the OpenMeter customer profile.

For organization-first billing, keep the same organization key everywhere:

openmeterPlugin({
  openmeterClient,
  organization: {
    enabled: true,
    resolveKey: ({ organization }) => `org:${organization.id}`,
    resolveSubject: ({ organization }) => `org:${organization.id}`,
  },
});
await authClient.openmeter.organization.events.ingest({
  organizationId: "org_123",
  type: "ai-tokens",
  data: { model: "gpt-4.1", tokens: 100 },
});

const access = await authClient.openmeter.organization.customer.access({
  query: { organizationId: "org_123" },
});

Usage Events

OpenMeter ingests CloudEvents-style usage events. The plugin defaults missing source to better-auth and missing subject to the resolved customer subject.

await authClient.openmeter.events.ingest({
  type: "ai-tokens",
  data: {
    model: "gpt-4.1",
    kind: "output",
    tokens: 850,
  },
});

await authClient.openmeter.events.ingest({
  events: [
    {
      type: "api-request",
      data: { route: "/v1/messages", duration_ms: 240 },
    },
    {
      type: "ai-tokens",
      data: { model: "gpt-4.1", tokens: 1200 },
    },
  ],
});

Entitlements

The plugin uses OpenMeter's customer-scoped entitlement APIs.

const { data, error } = await authClient.openmeter.entitlement.value({
  query: {
    featureKey: "gpt_4_tokens",
  },
});

if (!error && data?.hasAccess) {
  // allow feature usage
}

Catalog Setup

Use a billing catalog when you want one app-owned definition for plans, features, prices, and provider mappings. The catalog can compile OpenMeter entitlements and payment-provider product/price setup data without making Stripe, Polar, Razorpay, Creem, Dodo, or Autumn the source of truth.

import {
  compileOpenMeterTopupGrant,
  compilePaymentCatalog,
  createCatalogEntitlementMapper,
  defineBillingCatalog,
} from "better-auth-openmeter-plugin/catalog";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";

export const catalog = defineBillingCatalog({
  meters: {
    tokens: {
      key: "tokens",
      eventType: "ai.tokens",
      aggregation: "sum",
      valueProperty: "tokens",
    },
  },
  features: {
    aiTokens: {
      key: "ai_tokens",
      type: "metered",
      meter: "tokens",
    },
    apiAccess: {
      key: "api_access",
      type: "boolean",
    },
    prioritySupport: {
      key: "priority_support",
      type: "boolean",
    },
  },
  plans: {
    pro: {
      name: "Pro",
      providerIds: {
        stripe: "prod_...",
      },
      entitlements: {
        aiTokens: { amount: 100000, reset: "month" },
        apiAccess: true,
      },
      prices: {
        monthly: {
          amount: 2000,
          currency: "USD",
          interval: "month",
          providerIds: {
            stripe: "price_...",
          },
        },
      },
    },
  },
  addons: {
    prioritySupport: {
      compatiblePlans: ["pro"],
      entitlements: {
        prioritySupport: true,
      },
      prices: {
        monthly: {
          amount: 1000,
          currency: "USD",
          interval: "month",
        },
      },
    },
  },
  topups: {
    tokenPack1m: {
      feature: "aiTokens",
      amount: 1_000_000,
      grant: {
        priority: 1,
        expiration: {
          duration: "YEAR",
          count: 1,
        },
      },
      prices: {
        oneTime: {
          amount: 1000,
          currency: "USD",
          interval: "one_time",
        },
      },
    },
  },
});

openmeterBillingAdapter({
  catalog,
});

const stripeSetup = compilePaymentCatalog(catalog, "stripe");
const tokenGrant = compileOpenMeterTopupGrant(catalog, "tokenPack1m");

The catalog now distinguishes three sellable shapes:

  • plans: recurring subscription definitions
  • addons: subscription extensions such as extra seats, feature upgrades, or recurring capacity
  • topups: one-time metered balance or capacity purchases that should become OpenMeter customer grants after payment succeeds

plans are recurring-first by design. Plan prices cannot use interval: "one_time". Generic one-time commerce such as onboarding, migration, or setup fees is out of scope for this v1 OpenMeter-oriented DSL. Use topups only when the purchase adds metered balance or capacity to an existing entitlement.

When compatiblePlans is omitted on an add-on or top-up, the compiler treats it as compatible with every catalog plan.

Provider compilation keeps the app catalog as the source of truth and emits an explicit strategy per item:

  • Stripe: recurring add-ons compile as subscription items, one-time add-ons as invoice items, top-ups as one-time checkout charges
  • Razorpay: plans compile to subscription plans, add-ons map to upfront subscription add-ons, top-ups require a custom payment flow and are surfaced as compatibility warnings
  • Dodo Payments: plans and add-ons stay as subscription products/add-ons, top-ups compile as one-time checkout products
  • OpenMeter: plans and add-ons compile to entitlements, while top-ups compile to customer grants

Use compilePaymentCatalog(catalog, provider, { strict: true }) when CI should fail on provider-specific incompatibilities instead of returning warnings.

For mature applications that already use managed plans/subscriptions elsewhere, keep the same catalog for validation and event metadata, but disable OpenMeter entitlement creation from billing events:

openmeterBillingAdapter({
  catalog,
  entitlementMode: "none",
  onBillingEvent(event) {
    // Mirror or audit billing state without changing OpenMeter entitlements.
  },
});

Adapters

The core plugin stays focused on OpenMeter customers, subjects, usage events, and entitlements. Optional adapters integrate other Better Auth plugins without coupling every provider into the core.

API Key Adapter

Use the API key adapter with @better-auth/api-key to meter successful API key verification calls.

import { apiKey } from "@better-auth/api-key";
import { betterAuth } from "better-auth";
import { openmeterPlugin } from "better-auth-openmeter-plugin";
import { openmeterApiKeyAdapter } from "better-auth-openmeter-plugin/adapters/api-key";

export const auth = betterAuth({
  plugins: [
    apiKey(),
    openmeterPlugin({
      apiKey: process.env.OPENMETER_API_KEY!,
    }),
    openmeterApiKeyAdapter({
      resolveSubject: ({ apiKey }) =>
        typeof apiKey.metadata?.openmeterSubject === "string"
          ? apiKey.metadata.openmeterSubject
          : apiKey.referenceId,
    }),
  ],
});

By default, the adapter listens to /api-key/verify and ingests better-auth.api-key.verified with API key id, name, owner reference, remaining count, rate-limit fields, permissions, and metadata.

Billing Adapter

Payment gateways should integrate through one billing category instead of duplicating OpenMeter entitlement logic per provider.

import {
  applyCatalogTopupGrant,
  applyOpenMeterBillingEvent,
  openmeterBillingAdapter,
} from "better-auth-openmeter-plugin/adapters/billing";

openmeterBillingAdapter({
  catalog,
});

You can still provide mapPlanToEntitlements directly for custom logic. When a catalog is provided and entitlementMode is not "none", the adapter maps event.plan to catalog entitlements automatically.

Provider packages should translate gateway-specific callbacks or webhooks into generic billing events:

await applyOpenMeterBillingEvent(
  {
    type: "subscription.active",
    provider: "stripe",
    customerIdOrKey: "cus_123",
    subject: "org_123",
    referenceId: "org_123",
    customerType: "organization",
    plan: "pro",
    subscriptionId: "sub_123",
  },
  ctx,
  billingOptions,
);

To grant a catalog top-up after a one-time payment succeeds, use the runtime helper:

await applyCatalogTopupGrant(
  {
    customerIdOrKey: "org_123",
    subject: "org_123",
    provider: "stripe",
    idempotencyKey: "checkout:cs_123",
    paymentId: "pi_123",
    topup: "tokenPack1m",
    metadata: {
      checkoutSessionId: "cs_123",
    },
  },
  ctx,
  {
    catalog,
  },
);

This compiles the top-up into an OpenMeter customer grant, creates it through customers.entitlements.createGrant(...), and ingests better-auth.billing.topup.applied by default for auditability.

Top-up grants use metadata idempotency by default when idempotencyKey or paymentId is provided. The helper checks existing grants on the same customer and feature before creating a new grant, stores the key in grant metadata, and returns created: false when a retry maps to an existing grant. The audit event also uses a stable CloudEvents id so OpenMeter can deduplicate retried audit ingestion. For production webhooks, still keep an app-owned payment ledger with a unique payment/event key; the OpenMeter lookup is a convenience guard, not a replacement for race-safe payment reconciliation. Set idempotency: "none" if your ledger owns all deduplication.

This is the intended path for Stripe, Razorpay, Polar, and custom billing providers. Polar already has usage-metering features, so only bridge it when OpenMeter is the source of truth for entitlements.

Billing providers should resolve customerIdOrKey to the same value your core plugin returns from customer.resolveKey or organization.resolveKey. Gateway customer IDs such as Stripe cus_... or Razorpay customer IDs are usually best kept in event metadata unless you intentionally key OpenMeter customers by the gateway customer ID.

Razorpay Billing Provider

The Razorpay provider is a callback bridge for better-auth-razorpay-plugin. It does not add a hard dependency on Razorpay; wire its callbacks into the Razorpay subscription callbacks you already pass.

import { betterAuth } from "better-auth";
import { openmeterPlugin } from "better-auth-openmeter-plugin";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { razorpayBillingProvider } from "better-auth-openmeter-plugin/adapters/razorpay";
import { razorpayPlugin } from "better-auth-razorpay-plugin";

const razorpayProvider = razorpayBillingProvider({
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pro") return [];

      return [
        {
          featureKey: "ai_tokens",
          type: "metered",
          amount: 100000,
        },
      ];
    },
  },
  resolveCustomerIdOrKey: ({ subscription }) => subscription.referenceId,
  resolveSubject: ({ subscription }) => subscription.referenceId,
});

export const auth = betterAuth({
  plugins: [
    openmeterPlugin({ openmeterClient }),
    openmeterBillingAdapter({ provider: razorpayProvider }),
    razorpayPlugin({
      // ...
      subscription: {
        enabled: true,
        plans: [{ name: "pro", planId: "plan_..." }],
        ...razorpayProvider.callbacks,
      },
    }),
  ],
});

The provider maps Razorpay callbacks to generic billing events: onSubscriptionActivated becomes subscription.active, charged/renewed callbacks become invoice.paid, cancellation becomes subscription.canceled, and status changes become subscription.updated.

Stripe Billing Provider

The Stripe provider is a callback bridge for @better-auth/stripe.

import { stripe } from "@better-auth/stripe";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { stripeBillingProvider } from "better-auth-openmeter-plugin/adapters/stripe";

const stripeProvider = stripeBillingProvider({
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pro") return [];
      return [{ featureKey: "ai_tokens", type: "metered", amount: 100000 }];
    },
  },
});

betterAuth({
  plugins: [
    openmeterPlugin({ openmeterClient }),
    openmeterBillingAdapter({ provider: stripeProvider }),
    stripe({
      stripeClient,
      stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
      subscription: {
        enabled: true,
        plans: [{ name: "pro", priceId: "price_..." }],
        ...stripeProvider.callbacks,
      },
    }),
  ],
});

The provider maps onSubscriptionComplete to subscription.active, onSubscriptionCreated to subscription.created, update/cancel/delete callbacks to their equivalent generic billing events.

Polar Billing Provider

The Polar provider bridges @polar-sh/better-auth webhook callbacks into OpenMeter billing events. Use this only when OpenMeter is the source of truth for usage and entitlements; Polar also has its own usage plugin.

import { polar, webhooks } from "@polar-sh/better-auth";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { polarBillingProvider } from "better-auth-openmeter-plugin/adapters/polar";

const polarProvider = polarBillingProvider({
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pro") return [];
      return [{ featureKey: "ai_tokens", type: "metered", amount: 100000 }];
    },
  },
});

betterAuth({
  plugins: [
    openmeterPlugin({ openmeterClient }),
    openmeterBillingAdapter({ provider: polarProvider }),
    polar({
      client: polarClient,
      createCustomerOnSignUp: true,
      use: [
        webhooks({
          secret: process.env.POLAR_WEBHOOK_SECRET!,
          ...polarProvider.callbacks,
        }),
      ],
    }),
  ],
});

The provider reads referenceId from Polar metadata when present. Provide resolveCustomerIdOrKey if your Polar payloads use a different convention.

Creem Billing Provider

The Creem provider bridges the official @creem_io/better-auth webhook and access-control callbacks into OpenMeter billing events. Use the event-specific callbacks when you want a direct webhook mirror, or the high-level onGrantAccess / onRevokeAccess callbacks when Creem should decide whether a subscription currently grants access.

import { creem } from "@creem_io/better-auth";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { creemBillingProvider } from "better-auth-openmeter-plugin/adapters/creem";

const creemProvider = creemBillingProvider({
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pro") return [];
      return [{ featureKey: "ai_tokens", type: "metered", amount: 100000 }];
    },
  },
});

betterAuth({
  plugins: [
    openmeterPlugin({ openmeterClient }),
    openmeterBillingAdapter({ provider: creemProvider }),
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
      persistSubscriptions: true,
      ...creemProvider.callbacks,
    }),
  ],
});

The provider reads referenceId from Creem metadata by default and maps subscription callbacks to generic events such as subscription.active, invoice.paid, subscription.canceled, and subscription.expired.

Dodo Payments Billing Provider

The Dodo provider bridges @dodopayments/better-auth webhook callbacks into OpenMeter billing events. Dodo's webhooks plugin can invoke both onPayload and granular handlers for the same webhook, so the default spreadable callbacks object intentionally contains only granular handlers. Use dodoProvider.onPayload by itself if you prefer catch-all ingestion.

import { dodopayments, checkout, portal, webhooks } from "@dodopayments/better-auth";
import DodoPayments from "dodopayments";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { dodoBillingProvider } from "better-auth-openmeter-plugin/adapters/dodo";

const dodoProvider = dodoBillingProvider({
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pdt_pro") return [];
      return [{ featureKey: "ai_tokens", type: "metered", amount: 100000 }];
    },
  },
});

betterAuth({
  plugins: [
    openmeterPlugin({ openmeterClient }),
    openmeterBillingAdapter({ provider: dodoProvider }),
    dodopayments({
      client: new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY! }),
      createCustomerOnSignUp: true,
      use: [
        checkout({
          products: [{ productId: "pdt_pro", slug: "pro" }],
          successUrl: "/dashboard",
          authenticatedUsersOnly: true,
        }),
        portal(),
        webhooks({
          webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
          ...dodoProvider.callbacks,
        }),
      ],
    }),
  ],
});

The provider reads referenceId from webhook metadata by default and maps Dodo payment/subscription callbacks to generic events such as invoice.paid, subscription.active, subscription.updated, and subscription.canceled.

Autumn Billing Provider

Autumn's Better Auth plugin is not webhook-driven. It resolves a customer, then exposes billing, check, track, and portal endpoints through Better Auth. The OpenMeter adapter therefore focuses on the stable integration points: sharing the same user/organization customer identity and explicitly mirroring Autumn billing state into OpenMeter after your attach/update flow confirms the state you want OpenMeter to reflect.

import { autumn } from "autumn-js/better-auth";
import { organization } from "better-auth/plugins";
import { openmeterBillingAdapter } from "better-auth-openmeter-plugin/adapters/billing";
import { autumnBillingProvider } from "better-auth-openmeter-plugin/adapters/autumn";

const autumnProvider = autumnBillingProvider({
  customerScope: "organization",
  billing: {
    openmeterClient,
    mapPlanToEntitlements(event) {
      if (event.plan !== "pro") return [];
      return [{ featureKey: "ai_tokens", type: "metered", amount: 100000 }];
    },
  },
});

export const auth = betterAuth({
  plugins: [
    organization(),
    openmeterPlugin({
      openmeterClient,
      organization: { enabled: true },
    }),
    openmeterBillingAdapter({ provider: autumnProvider }),
    autumn({
      customerScope: "organization",
      identify: autumnProvider.identify,
    }),
  ],
});

// After an Autumn attach/update/check flow confirms the customer should have
// access, mirror that state into OpenMeter:
await autumnProvider.handleBillingState({
  type: "subscription.active",
  identity: { session, organization },
  productId: "pro",
  subscriptionId: "sub_...",
});

If Autumn is your entitlement source of truth, use this adapter only for shared identity and audit events. If OpenMeter is the entitlement source of truth, call handleBillingState at the point where your application has confirmed the Autumn state to mirror.

Migration and Backfill

After enabling the plugin, run your Better Auth migration/generation step so openmeterCustomerId exists on user and, when organization support is enabled, on organization.

New users and organizations can be created automatically with createCustomerOnSignUp and createCustomerOnOrganizationCreate. Existing rows need a one-time backfill. Use your application's server-side auth context or database job to iterate users/organizations and call the sync endpoints with authenticated headers for the relevant user:

for (const user of users) {
  await fetch(`${baseUrl}/api/auth/openmeter/customer/sync`, {
    method: "POST",
    headers: await authenticatedHeadersForUser(user.id),
  });
}

for (const organization of organizations) {
  await fetch(`${baseUrl}/api/auth/openmeter/organization/customer/sync`, {
    method: "POST",
    headers: {
      ...(await authenticatedHeadersForUser(organization.ownerId)),
      "content-type": "application/json",
    },
    body: JSON.stringify({ organizationId: organization.id }),
  });
}

Backfill is idempotent when resolveKey is deterministic: OpenMeter is looked up by key, then created or updated, and the generated OpenMeter id is stored back as openmeterCustomerId.

React Query Helpers

import {
  useIngestOpenMeterEvent,
  useOpenMeterEntitlementValue,
} from "better-auth-openmeter-plugin/react";

export function TokenButton({ authClient }: { authClient: any }) {
  const entitlement = useOpenMeterEntitlementValue(authClient, "gpt_4_tokens");
  const ingest = useIngestOpenMeterEvent(authClient);

  return (
    <button
      type="button"
      disabled={!entitlement.data?.hasAccess || ingest.isPending}
      onClick={() =>
        ingest.mutate({
          type: "ai-tokens",
          data: { model: "gpt-4.1", tokens: 100 },
        })
      }
    >
      Generate
    </button>
  );
}

Development

Pull requests run the CI workflow, which installs dependencies with npm ci, then runs typecheck, unit tests, build, and npm pack --dry-run.

Publishing is handled by the manual Publish workflow. Configure an npm automation token as the NPM_TOKEN repository or environment secret, bump the package version, merge through a PR, then run the workflow from main.