> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getruba.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Make Billing Follow Better Auth Identity

> Connect authenticated users to customers, checkout, usage, webhooks, and portal data

export const RubaSocials = () => {
  if (typeof document === "undefined") {
    return null;
  }
  const inject = () => {
    const footer = document.getElementById("footer");
    if (!footer || document.getElementById("ruba-socials")) {
      return false;
    }
    const isDark = document.querySelector("html.dark") || document.querySelector('html[class*="dark"]');
    const lineColor = isDark ? "#383838" : "#e8e8ed";
    const iconColor = isDark ? "#8e8e93" : "#86868b";
    const markColor = isDark ? "#ffffff" : "#08080c";
    const hoverColor = "#0071e3";
    const div = document.createElement("div");
    div.id = "ruba-socials";
    div.style.cssText = `display:flex;align-items:center;gap:20px;padding-top:20px;margin-top:20px;border-top:1px solid ${lineColor};width:100%`;
    const logo = `<a href="https://getruba.com" style="display:inline-flex;height:24px;align-items:center;gap:8px;text-decoration:none;color:${markColor};line-height:1;"><svg width="24" height="24" viewBox="0 0 120 120" aria-hidden="true" style="display:block;flex:none;"><g transform="translate(2.5 0)"><path d="M19 25h59L65.43 47H19a4 4 0 0 1-4-4V29a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M19 73h31.57L38 95H19a4 4 0 0 1-4-4V77a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M85 25h25L70 95H45Z" fill="#007AFF"/></g></svg><span style="display:inline-flex;height:24px;align-items:center;font-weight:600;font-size:15px;color:${markColor};letter-spacing:-0.01em;line-height:1;">Ruba</span></a>`;
    const spacer = `<div style="flex:1"></div>`;
    const ig = `<a href="https://instagram.com/getruba" target="_blank" rel="noopener" aria-label="Instagram" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg></a>`;
    const li = `<a href="https://linkedin.com/company/getruba" target="_blank" rel="noopener" aria-label="LinkedIn" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg></a>`;
    const x = `<a href="https://x.com/getruba" target="_blank" rel="noopener" aria-label="X" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a>`;
    const github = `<a href="https://github.com/rubadot" target="_blank" rel="noopener" aria-label="GitHub" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .7a11.5 11.5 0 0 0-3.64 22.4c.58.1.79-.25.79-.56v-2.24c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.78 1.2 1.78 1.2 1.04 1.77 2.72 1.26 3.38.96.1-.75.4-1.26.74-1.55-2.57-.3-5.27-1.29-5.27-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.47.11-3.05 0 0 .97-.31 3.16 1.18A10.96 10.96 0 0 1 12 6.09c.98 0 1.94.13 2.86.39 2.2-1.49 3.16-1.18 3.16-1.18.63 1.58.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.28 5.68.42.36.79 1.06.79 2.14v3.28c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .7Z"/></svg></a>`;
    div.innerHTML = logo + spacer + ig + li + x + github;
    footer.appendChild(div);
    return true;
  };
  if (!document.documentElement.dataset.rubaSocialsObserver) {
    document.documentElement.dataset.rubaSocialsObserver = "true";
    const observer = new MutationObserver(() => {
      if (!document.getElementById("ruba-socials")) {
        inject();
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  setTimeout(() => {
    if (!inject()) {
      const interval = setInterval(() => {
        if (inject()) {
          clearInterval(interval);
        }
      }, 500);
      setTimeout(() => clearInterval(interval), 10000);
    }
  }, 300);
  return null;
};

<link rel="stylesheet" href="/style.css" />

<RubaSocials />

`@getruba/better-auth` lets [Better Auth](https://github.com/better-auth/better-auth) identity become the customer key used across Ruba. Enable only the surfaces your application needs:

| Surface       | What the plugin adds                                               |
| ------------- | ------------------------------------------------------------------ |
| Identity sync | Create a Ruba customer at signup and remove it after user deletion |
| Checkout      | Start checkout for product IDs or configured slugs                 |
| Usage         | Ingest events and read the signed-in customer's meter state        |
| Webhooks      | Verify Ruba delivery and dispatch typed callbacks                  |
| Portal        | Open self-service and query customer-scoped commerce records       |

## Install the identity bridge

Add Better Auth, the Ruba plugin, and the core SDK:

<Tabs>
  <Tab title="npm">
    ```bash Terminal theme={"system"}
    npm install better-auth @getruba/better-auth @getruba/sdk
    ```
  </Tab>

  <Tab title="yarn">
    ```bash Terminal theme={"system"}
    yarn add better-auth @getruba/better-auth @getruba/sdk
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash Terminal theme={"system"}
    pnpm add better-auth @getruba/better-auth @getruba/sdk
    ```
  </Tab>

  <Tab title="bun">
    ```bash Terminal theme={"system"}
    bun add better-auth @getruba/better-auth @getruba/sdk
    ```
  </Tab>
</Tabs>

## Establish the shared customer identity

<Steps>
  <Step title="Create a server credential">
    Generate an Organization Access Token in Ruba settings and keep it in the server environment.

    ```bash .env theme={"system"}
    RUBA_ACCESS_TOKEN=...
    ```
  </Step>

  <Step title="Compose the server plugins">
    Create one Ruba SDK client, pass it to `ruba()`, and select the capability modules under `use`:

    * `checkout` starts buying flows.
    * `portal` exposes customer self-service and records.
    * `usage` sends events and reads meters.
    * `webhooks` receives signed state changes.

    ```typescript icon="square-js" auth.ts theme={"system"}
    import { betterAuth } from "better-auth";
    import { ruba, checkout, portal, usage, webhooks } from "@getruba/better-auth"; // [!code ++]
    import { Ruba } from "@getruba/sdk"; // [!code ++]

    const rubaClient = new Ruba({ // [!code ++]
        accessToken: process.env.RUBA_ACCESS_TOKEN, // [!code ++]
        // Use 'sandbox' if you're using the Ruba Sandbox environment
        // Remember that access tokens, products, etc. are completely separated between environments.
        // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
        server: 'sandbox' // [!code ++]
    }); // [!code ++]

    const auth = betterAuth({
        // ... Better Auth config
        plugins: [
            ruba({ // [!code ++]
                client: rubaClient, // [!code ++]
                createCustomerOnSignUp: true, // [!code ++]
                use: [ // [!code ++]
                    checkout({ // [!code ++]
                        products: [ // [!code ++]
                            { // [!code ++]
                                productId: "123-456-789", // ID of Product from Ruba Dashboard // [!code ++]
                                slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro // [!code ++]
                            } // [!code ++]
                        ], // [!code ++]
                        successUrl: "/success?checkout_id={CHECKOUT_ID}", // [!code ++]
                        authenticatedUsersOnly: true // [!code ++]
                    }), // [!code ++]
                    portal(), // [!code ++]
                    usage(), // [!code ++]
                    webhooks({ // [!code ++]
                        secret: process.env.RUBA_WEBHOOK_SECRET, // [!code ++]
                        onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes // [!code ++]
                        onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.) // [!code ++]
                        ...  // Over 25 granular webhook handlers // [!code ++]
                        onPayload: (payload) => // Catch-all for all events // [!code ++]
                    }) // [!code ++]
                ], // [!code ++]
            }) // [!code ++]
        ]
    });
    ```

    #### Root configuration

    ```typescript theme={"system"}
    // ...

    const auth = betterAuth({
      // ... Better Auth config
      plugins: [
        ruba({
          client: rubaClient, // [!code ++]
          createCustomerOnSignUp: true, // [!code ++]
          getCustomerCreateParams: ({ user }, request) => ({ // [!code ++]
            metadata: { // [!code ++]
              myCustomProperty: 123, // [!code ++]
            }, // [!code ++]
          }), // [!code ++]
          use: [ // [!code ++]
            // This is where you add Ruba plugins // [!code ++]
          ], // [!code ++]
        }),
      ],
    });
    ```

    | Option                    | Meaning                                                  |
    | ------------------------- | -------------------------------------------------------- |
    | `client`                  | Required Ruba SDK instance                               |
    | `createCustomerOnSignUp`  | Creates the matching customer during signup when enabled |
    | `getCustomerCreateParams` | Adds metadata or other create-customer fields            |
    | `use`                     | Capability modules enabled for this Better Auth instance |
  </Step>

  <Step title="Expose typed client methods">
    Add `rubaClient()` to the browser-facing Better Auth client. Billing authority still remains in the server configuration.

    ```typescript icon="square-js" auth-client.ts theme={"system"}
    import { createAuthClient } from "better-auth/react";
    import { rubaClient } from "@getruba/better-auth/client"; // [!code ++]
    import { organizationClient } from "better-auth/client/plugins"; // [!code ++]

    // All Ruba plugins, etc. should be attached to BetterAuth server
    export const authClient = createAuthClient({ // [!code ++]
      plugins: [rubaClient()], // [!code ++]
    }); // [!code ++]
    ```
  </Step>
</Steps>

## Keep user and customer lifecycle aligned

Enable `createCustomerOnSignUp` to create a customer with the Better Auth user ID stored as `externalId`. That stable key removes the need for a separate mapping table.

For deletion, enable Better Auth's `deleteUser` flow and remove the external Ruba customer from `afterDelete`:

```typescript icon="square-js" Customer Deletion Example theme={"system"}
const auth = betterAuth({
  user: {
    // [!code ++]
    deleteUser: {
      // [!code ++]
      enabled: true, // [!code ++]
      afterDelete: async (user, request) => {
        // [!code ++]
        await ruba.customers.deleteExternal({
          // [!code ++]
          externalId: user.id, // [!code ++]
        }); // [!code ++]
      }, // [!code ++]
    }, // [!code ++]
  }, // [!code ++]
});
```

## Start checkout from an authenticated session

[Source code](https://github.com/Rubadot/ruba/blob/main/clients/packages/better-auth/src/plugins/checkout.ts)

Add `checkout` under `use`. Its configuration separates catalog mapping from browser behavior:

| Option                   | Effect                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `products`               | Maps friendly slugs to product IDs, either statically or asynchronously                    |
| `successUrl`             | Relative or absolute completion target; `{CHECKOUT_ID}` inserts the session ID             |
| `returnUrl`              | Displays a route back to your application                                                  |
| `authenticatedUsersOnly` | Requires a session and binds its customer when true; permits anonymous checkout when false |
| `theme`                  | Forces `light` or `dark`                                                                   |

<Steps>
  <Step title="Register checkout on the server">
    Place the configured `checkout()` module in the root Ruba plugin's `use` array.

    ```typescript icon="square-js" Checkout Plugin Example theme={"system"}
    import {
      ruba,
      checkout // [!code ++]
    } from "@getruba/better-auth";

    const auth = betterAuth({
        // ... Better Auth config
        plugins: [
            ruba({
                ...
                use: [
                    checkout({ // [!code ++]
                        // Optional field - will make it possible to pass a slug to checkout instead of Product ID
                        products: [ { productId: "123-456-789", slug: "pro" } ], // [!code ++]
                        // Relative path or absolute URL to redirect to when checkout is successfully completed
                        successUrl: "/success?checkout_id={CHECKOUT_ID}", // [!code ++]
                        // Whether you want to allow unauthenticated checkout sessions or not
                        authenticatedUsersOnly: true, // [!code ++]
                        // An optional URL which renders a back-button in the Checkout
                        returnUrl: "https://myapp.com" // [!code ++]
                    }) // [!code ++]
                ],
            })
        ]
    });
    ```
  </Step>

  <Step title="Invoke it from the client">
    Call `authClient.checkout()` with product IDs or one configured slug. `referenceId` is copied into checkout, order, and subscription metadata.

    ```typescript icon="square-js" BetterAuth Checkout with Ruba Example theme={"system"}
    await authClient.checkout({
      // Ruba Product IDs
      products: ["e651f46d-ac20-4f26-b769-ad088b123df2"], // [!code ++]
      // OR
      // if "products" in passed in the checkout plugin's config, you may pass the slug
      // slug: "pro", // [!code ++]
    });
    ```

    With Better Auth Organizations, use the organization ID as `referenceId` to associate the resulting commerce records with that team rather than only the individual session.

    ```typescript icon="square-js" BetterAuth Checkout with Ruba Organization Example theme={"system"}
    const organizationId = (await authClient.organization.list())?.data?.[0]?.id,

    await authClient.checkout({
        // Any Ruba Product ID can be passed here
        products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
        // Or, if you setup "products" in the Checkout Config, you can pass the slug
        slug: 'pro',
        // Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object
        referenceId: organizationId
    });
    ```
  </Step>
</Steps>

## Connect authenticated usage to meters

[Source code](https://github.com/Rubadot/ruba/blob/main/clients/packages/better-auth/src/plugins/usage.ts)

The `usage` module adds event ingestion and customer-meter reads. Register it in `use` after the application's [usage model](/features/usage-based-billing/introduction) exists.

```typescript icon="square-js" Usage Plugin Example theme={"system"}
import {
  ruba, checkout, portal,
  usage // [!code ++]
} from "@getruba/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        ruba({
            ...
            use: [
                checkout(...),
                portal(),
                usage() // [!code ++]
            ],
        })
    ]
});
```

### Record authoritative activity

<Warning>
  Billable events belong on the server route that performs the work. A browser can forge or skip its own usage claims.
</Warning>

With signup sync enabled, `session.user.id` is already the customer's `externalId`. Reuse the configured SDK client inside the protected work route:

```typescript icon="square-js" app/api/ai/video/route.ts (Next.js App Router) theme={"system"}
import { auth } from "@/lib/auth";
import { rubaClient } from "@/lib/ruba";
import { headers } from "next/headers";

export async function POST(request: Request) {
  const session = await auth.api.getSession({ headers: await headers() });

  if (!session) {
    return new Response("Unauthorized", { status: 401 });
  }

  // Run the metered work on the server
  const { video, tokensConsumed } = await makeNewVideo(request);

  // Ingest the resulting usage against the authenticated user
  await rubaClient.events.ingest({
    events: [
      {
        name: "ai-video",
        externalCustomerId: session.user.id,
        metadata: {
          tokensConsumed,
        },
      },
    ],
  });

  return Response.json({ video });
}
```

Each event needs a `name`, the Better Auth user ID as `externalCustomerId`, and optional string, number, or boolean `metadata` used by filters and aggregations.

#### Non-authoritative browser signals

`authClient.usage.ingestion(...)` sends through the Better Auth server and attaches the signed-in user automatically:

```typescript icon="square-js" auth-client.ts theme={"system"}
const { data: ingested } = await authClient.usage.ingestion({
  event: "file-uploads",
  metadata: {
    uploadedFiles: 12,
  },
});
```

<Warning>
  Treat this endpoint like analytics input. The user controls its payload, so it must not determine a bill or protected quota.
</Warning>

### Read the user's meter position

`authClient.usage.meters()` lists Customer Meters for the active session. `page` starts at 1 and `limit` caps each page.

```typescript icon="square-js" Customer Meters with Usage Plugin Example theme={"system"}
const { data: customerMeters } = await authClient.usage.meters.list({
  query: {
    page: 1,
    limit: 10,
  },
});
```

The response combines customer identity, meter definition, consumed units, credited units, and the remaining balance between them.

## Bring asynchronous changes into Better Auth

[Source code](https://github.com/Rubadot/ruba/blob/main/clients/packages/better-auth/src/plugins/webhooks.ts)

The webhook module owns `/api/auth/ruba/webhooks`, verifies delivery, and routes events to your callbacks.

<Steps>
  <Step title="Register the generated endpoint">
    In Ruba webhook settings, create an endpoint ending in `/api/auth/ruba/webhooks`.
  </Step>

  <Step title="Store its secret">
    Save the issued value as `RUBA_WEBHOOK_SECRET` in the server environment.

    ```bash .env theme={"system"}
    RUBA_WEBHOOK_SECRET="..."
    ```
  </Step>

  <Step title="Bind callbacks">
    Add `webhooks()` under `use`, pass the secret, and implement the events your application consumes.

    ```typescript icon="square-js" Webhooks Plugin Example theme={"system"}
    import {
      ruba,
      webhooks // [!code ++]
    } from "@getruba/better-auth";

    const auth = betterAuth({
        // ... Better Auth config
        plugins: [
            ruba({
                ...
                use: [
                    webhooks({ // [!code ++]
                        secret: process.env.RUBA_WEBHOOK_SECRET, // [!code ++]
                        onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes // [!code ++]
                        onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.) // [!code ++]
                        ...  // Over 25 granular webhook handlers // [!code ++]
                        onPayload: (payload) => // Catch-all for all events // [!code ++]
                    }) // [!code ++]
                ],
            })
        ]
    });
    ```
  </Step>
</Steps>

### Choose callback granularity

Use `onPayload` as a catch-all or register callbacks around the domain your application owns:

| Domain             | Available callbacks                                                                                                                                     |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Checkout           | `onCheckoutCreated`, `onCheckoutUpdated`                                                                                                                |
| Orders and refunds | `onOrderCreated`, `onOrderPaid`, `onOrderRefunded`, `onRefundCreated`, `onRefundUpdated`                                                                |
| Subscriptions      | `onSubscriptionCreated`, `onSubscriptionUpdated`, `onSubscriptionActive`, `onSubscriptionCanceled`, `onSubscriptionRevoked`, `onSubscriptionUncanceled` |
| Catalog            | `onProductCreated`, `onProductUpdated`, `onBenefitCreated`, `onBenefitUpdated`                                                                          |
| Entitlements       | `onBenefitGrantCreated`, `onBenefitGrantUpdated`, `onBenefitGrantRevoked`                                                                               |
| Customers          | `onCustomerCreated`, `onCustomerUpdated`, `onCustomerDeleted`, `onCustomerStateChanged`                                                                 |
| Organization       | `onOrganizationUpdated`                                                                                                                                 |

## Query commerce through the signed-in customer

[Source code](https://github.com/Rubadot/ruba/blob/main/clients/packages/better-auth/src/plugins/portal.ts)

```typescript icon="square-js" Portal module theme={"system"}
import {
  ruba, checkout,
  portal // [!code ++]
} from "@getruba/better-auth";

const auth = betterAuth({
    // ... Better Auth config
    plugins: [
        ruba({
            ...
            use: [
                checkout(...),
                portal({
                  returnUrl: "https://myapp.com", // An optional URL which renders a back-button in the Customer Portal
                }) // [!code ++]
            ],
        })
    ]
});
```

The portal module adds customer-scoped methods under `authClient.customer`.

### Open self-service

`authClient.customer.portal()` redirects the user to Ruba's portal for orders, subscriptions, and benefits.

```typescript icon="square-js" Open Customer Portal Example theme={"system"}
await authClient.customer.portal();
```

### Fetch one access snapshot

`authClient.customer.state()` returns identity, direct active subscriptions, granted benefits, and active meters with their balances.

```typescript icon="square-js" Retrieve Customer State Example theme={"system"}
const { data: customerState } = await authClient.customer.state();
```

Use that snapshot for access decisions. It excludes subscriptions purchased by a parent organization; query subscriptions with the organization reference for that case. [Understand Customer State →](/integrate/customer-state)

### List individual record types

#### Granted benefits

The benefits method returns grants belonging to the active customer.

```typescript icon="square-js" List User Benefits Example theme={"system"}
const { data: benefits } = await authClient.customer.benefits.list({
  query: {
    page: 1,
    limit: 10,
  },
});
```

#### Orders

The orders method returns that customer's purchases and renewal records.

```typescript icon="square-js" List User Orders Example theme={"system"}
const { data: orders } = await authClient.customer.orders.list({
  query: {
    page: 1,
    limit: 10,
    productBillingType: "one_time", // or 'recurring'
  },
});
```

When a purchase belongs to a Better Auth Organization, pass its ID as `referenceId` and use that reference to find the organization's subscriptions.

```typescript icon="square-js" List Organization Subscriptions Example theme={"system"}
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,

const { data: subscriptions } = await authClient.customer.orders.list({
    query: {
	    page: 1,
		limit: 10,
		active: true,
        referenceId: organizationId
    },
});

const userShouldHaveAccess = subscriptions.some(
    sub => // Your logic to check subscription product or whatever.
)
```

#### Subscriptions

The subscriptions method returns relationships tied directly to the active customer.

```typescript icon="square-js" List User Subscriptions Example theme={"system"}
const { data: subscriptions } = await authClient.customer.subscriptions.list({
  query: {
    page: 1,
    limit: 10,
    active: true,
  },
});
```

<Danger>
  Direct-customer listing does not include subscriptions purchased through a parent organization.
</Danger>
