Support Tenant/Org-Scoped API Keys in Multi-Tenant Applications #1961

Open
opened 2026-03-13 09:16:43 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @issam-seghir on GitHub (Sep 18, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Currently, the API Key plugin associates API keys only with a "userId". This works well for single-tenant or user-centric applications, but in multi-tenant SaaS scenarios (e.g., e-commerce platforms like Shopify with multiple stores per user), API keys often need to be scoped at the tenant/store level, not the individual user.

For example:

  • In Shopify, API keys belong to the store, not the user who created them.
  • If a user leaves the organization, the API keys should remain valid as long as the store exists.
  • In my case, one user can own/manage multiple stores, and I need to issue API keys tied to the store (tenant), not to the user.

Why this matters:

  • Current schema requires "userId", which limits flexibility in multi-tenant designs.
  • It makes it harder to model real-world use cases where API integrations are linked to a business entity (store/org) instead of an individual.
  • Multi-tenant apps would need to hack around the schema or overload metadata to simulate tenant ownership.

Current vs. Expected behavior

Proposal:

  • Add first-class support for tenant-scoped API keys, in addition to user-scoped ones.

  • This could mean:

    1. Extending the schema with an optional "tenantId" field.
    2. Allowing API key creation with either "userId" or "tenantId".
    3. Updating verification logic to handle tenant context.
    4. Keeping metadata extensibility, but making tenant support a built-in option.

Example Schema Extension:

model ApiKey {
  id        String   @id @default(cuid())
  key       String   // hashed
  name      String?
  prefix    String?
  userId    String?  // for user-scoped keys
  tenantId  String?  // for tenant/store-scoped keys
  enabled   Boolean  @default(true)
  metadata  Json?
  expiresAt DateTime?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Example API Usage:

// Create a tenant-scoped API key
await auth.api.createApiKey({
  body: {
    name: "store-api-key",
    tenantId: "store_123", // instead of userId
    expiresIn: 60 * 60 * 24 * 30,
    prefix: "store_",
  },
});

Impact:

  • Backwards compatible (existing user-scoped API keys still work).
  • Unlocks usage for multi-tenant applications out-of-the-box.
  • Reduces need for workarounds in common SaaS scenarios.

What version of Better Auth are you using?

1.3.10

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26100",
    "cpuCount": 12,
    "cpuModel": "AMD Ryzen 5 PRO 4650G with Radeon Graphics",
    "totalMemory": "31.30 GB",
    "freeMemory": "4.36 GB"
  },
  "node": {
    "version": "v22.17.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.2"
    },
    {
      "name": "react",
      "version": "19.1.1"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "@prisma/client",
      "version": "^6.15.0"
    }
  ],
  "betterAuth": {
    "version": "^1.3.10",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Client, Backend, Package

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @issam-seghir on GitHub (Sep 18, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Currently, the [API Key plugin](https://better-auth.dev/docs/plugins/api-key) associates API keys only with a "userId". This works well for single-tenant or user-centric applications, but in **multi-tenant SaaS scenarios (e.g., e-commerce platforms like Shopify with multiple stores per user)**, API keys often need to be scoped at the **tenant/store level**, not the individual user. For example: * In Shopify, API keys belong to the **store**, not the user who created them. * If a user leaves the organization, the API keys should remain valid as long as the store exists. * In my case, one user can own/manage multiple stores, and I need to issue API keys tied to the **store (tenant)**, not to the user. **Why this matters:** * Current schema requires "userId", which limits flexibility in multi-tenant designs. * It makes it harder to model real-world use cases where API integrations are linked to a business entity (store/org) instead of an individual. * Multi-tenant apps would need to hack around the schema or overload metadata to simulate tenant ownership. ### Current vs. Expected behavior **Proposal:** * Add first-class support for **tenant-scoped API keys**, in addition to user-scoped ones. * This could mean: 1. Extending the schema with an optional "tenantId" field. 2. Allowing API key creation with either "userId" or "tenantId". 3. Updating verification logic to handle tenant context. 4. Keeping metadata extensibility, but making tenant support a built-in option. **Example Schema Extension:** ```prisma model ApiKey { id String @id @default(cuid()) key String // hashed name String? prefix String? userId String? // for user-scoped keys tenantId String? // for tenant/store-scoped keys enabled Boolean @default(true) metadata Json? expiresAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` **Example API Usage:** ```ts // Create a tenant-scoped API key await auth.api.createApiKey({ body: { name: "store-api-key", tenantId: "store_123", // instead of userId expiresIn: 60 * 60 * 24 * 30, prefix: "store_", }, }); ``` **Impact:** * Backwards compatible (existing user-scoped API keys still work). * Unlocks usage for multi-tenant applications out-of-the-box. * Reduces need for workarounds in common SaaS scenarios. ### What version of Better Auth are you using? 1.3.10 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26100", "cpuCount": 12, "cpuModel": "AMD Ryzen 5 PRO 4650G with Radeon Graphics", "totalMemory": "31.30 GB", "freeMemory": "4.36 GB" }, "node": { "version": "v22.17.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "next", "version": "15.5.2" }, { "name": "react", "version": "19.1.1" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "@prisma/client", "version": "^6.15.0" } ], "betterAuth": { "version": "^1.3.10", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Client, Backend, Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the enhancement label 2026-03-13 09:16:43 -05:00
Author
Owner

@issam-seghir commented on GitHub (Sep 18, 2025):

@dosubot what do you think about that ?
is there any workaround that or custom solution for the moment ?

@issam-seghir commented on GitHub (Sep 18, 2025): @dosubot what do you think about that ? is there any workaround that or custom solution for the moment ?
Author
Owner

@dosubot[bot] commented on GitHub (Sep 18, 2025):

Right now, Better Auth's API Key plugin is strictly userId-based—there's no built-in support for tenant/store-scoped API keys, and all endpoints, schema, and session logic require a userId (docs).

The only practical workaround is to enable the metadata field in the plugin options and store your tenant/store info there. You can then filter or query API keys by tenantId in your own app logic, but the plugin itself won't enforce tenant scoping or handle sessions for tenants (schema). This means you’ll need to manage tenant-level permissions and lifecycle outside of Better Auth, and API keys will still be tied to a user for authentication.

Trying to extend the schema with a tenantId field won’t work unless you fork the plugin and rewrite core logic, since userId is mandatory everywhere (plugin code). If true multi-tenant support is critical, you might want to track this as a feature request upstream.

Let me know if you want details on how to implement the metadata workaround or have questions about custom validators!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 18, 2025): <!-- Answer --> Right now, Better Auth's API Key plugin is strictly userId-based—there's no built-in support for tenant/store-scoped API keys, and all endpoints, schema, and session logic require a userId ([docs](https://github.com/better-auth/better-auth/blob/d1fe10962079c82920ea078a9847f65a59907b81/docs/content/docs/plugins/api-key.mdx)). The only practical workaround is to enable the metadata field in the plugin options and store your tenant/store info there. You can then filter or query API keys by tenantId in your own app logic, but the plugin itself won't enforce tenant scoping or handle sessions for tenants ([schema](https://github.com/better-auth/better-auth/blob/d1fe10962079c82920ea078a9847f65a59907b81/packages/better-auth/src/plugins/api-key/schema.ts)). This means you’ll need to manage tenant-level permissions and lifecycle outside of Better Auth, and API keys will still be tied to a user for authentication. Trying to extend the schema with a tenantId field won’t work unless you fork the plugin and rewrite core logic, since userId is mandatory everywhere ([plugin code](https://github.com/better-auth/better-auth/blob/d1fe10962079c82920ea078a9847f65a59907b81/packages/better-auth/src/plugins/api-key/index.ts)). If true multi-tenant support is critical, you might want to track this as a feature request upstream. Let me know if you want details on how to implement the metadata workaround or have questions about custom validators! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ac1cd229-5340-4df0-a018-9744c4530d67?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4746)
Author
Owner

@Pankaj3112 commented on GitHub (Sep 25, 2025):

I like how the Stripe plugin handles this:

  • They use a generic referenceId instead of a fixed tenantId.
  • If reference Id is not provided - by default it uses userId
  • An authorizeReference function is defined in the plugin options to handle access logic.

This approach keeps the schema flexible for user, tenants, orgs, or stores,
while enforcing explicit authorization. It might be worth adopting here.

@Pankaj3112 commented on GitHub (Sep 25, 2025): I like how the Stripe plugin handles this: * They use a generic `referenceId` instead of a fixed `tenantId`. * If reference Id is not provided - by default it uses userId * An `authorizeReference` function is defined in the plugin options to handle access logic. This approach keeps the schema flexible for user, tenants, orgs, or stores, while enforcing explicit authorization. It might be worth adopting here.
Author
Owner

@dosubot[bot] commented on GitHub (Dec 25, 2025):

Hi, @issam-seghir. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You requested support for tenant/store-scoped API keys in multi-tenant applications, as the current plugin only supports associating keys with userId.
  • The maintainer noted the plugin does not have built-in tenant support and suggested using metadata as a workaround.
  • True tenant scoping would require either a plugin fork or a new feature request.
  • A contributor proposed a flexible approach similar to the Stripe plugin, using a generic referenceId and an authorizeReference function for multi-tenant authorization, which was well received by the community.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open.
  • If I do not hear back within 7 days, I will automatically close this issue.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Dec 25, 2025): Hi, @issam-seghir. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You requested support for tenant/store-scoped API keys in multi-tenant applications, as the current plugin only supports associating keys with userId. - The maintainer noted the plugin does not have built-in tenant support and suggested using metadata as a workaround. - True tenant scoping would require either a plugin fork or a new feature request. - A contributor proposed a flexible approach similar to the Stripe plugin, using a generic referenceId and an authorizeReference function for multi-tenant authorization, which was well received by the community. **Next Steps:** - Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open. - If I do not hear back within 7 days, I will automatically close this issue. Thank you for your understanding and contribution!
Author
Owner

@issam-seghir commented on GitHub (Dec 25, 2025):

@dosubot yes, it's still relevant

@issam-seghir commented on GitHub (Dec 25, 2025): @dosubot yes, it's still relevant
Author
Owner

@jamesolrog commented on GitHub (Dec 28, 2025):

Have also come across this in trying to create "project-scoped" API-keys under better-auth-based organisations.

  • Users leaving organisations leaves API Key handling in a strange position.
  • Other aspects of the plugin, such as the approach to Secondary Storage can also feel a little wasteful when not utilising on user-based API Keys. For example, the "api-key:by-user:${userId}" just takes up space.

The work done on the API Key plugin is so fantastic, it would be very exciting to see it open up to support other forms of reference as @Pankaj3112 has suggested, even if it initially requires a bit of config via some new hook(s) to customise the validation steps.

@jamesolrog commented on GitHub (Dec 28, 2025): Have also come across this in trying to create "project-scoped" API-keys under better-auth-based organisations. - Users leaving organisations leaves API Key handling in a strange position. - Other aspects of the plugin, such as the approach to [Secondary Storage](https://www.better-auth.com/docs/plugins/api-key#secondary-storage) can also feel a little wasteful when not utilising on user-based API Keys. For example, the "api-key:by-user:${userId}" just takes up space. The work done on the API Key plugin is so fantastic, it would be very exciting to see it open up to support other forms of reference as @Pankaj3112 has suggested, even if it initially requires a bit of config via some new hook(s) to customise the validation steps.
Author
Owner

@heiwen commented on GitHub (Dec 29, 2025):

+1 on this, looking for the same functionality right now

@heiwen commented on GitHub (Dec 29, 2025): +1 on this, looking for the same functionality right now
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1961