Guidance Needed: Authenticating Server-to-Server Admin API Calls (401 Unauthorized) #1788

Closed
opened 2026-03-13 09:03:30 -05:00 by GiteaMirror · 12 comments
Owner

Originally created by @ayhamDev on GitHub (Aug 27, 2025).

Hello, I am using better-auth as the user authentication and management service for my application. My primary backend is built with Hono.js and acts as a trusted subsystem.

It's important to note that my backend has its own, separate system for managing administrators. The middleware I use (verifyRole('admin')) checks against my internal database, not against Better Auth roles.

My goal is for my backend server to be able to make administrative calls to the Better Auth API (e.g., listUsers, banUser, createUser) as a trusted service. These actions are triggered by my internal admins using my backend's dashboard, but the call to Better Auth should be authenticated as the server itself.

The Problem

When my backend server attempts to call an admin-only method like auth.api.listUsers(), the request fails with a 401 Unauthorized error. This is because the server-to-server call is being made without any authentication context.

Steps to Reproduce

  1. Configure a backend application (e.g., Hono, Express) with the better-auth client.
  2. Create an API endpoint that is protected by an internal admin verification system (one that is separate from Better Auth).
  3. Inside the endpoint's handler, make a call to an admin-only Better Auth method, like auth.api.listUsers({ query: {} }).

Example Code (Hono):

// This middleware successfully verifies the user against our INTERNAL admin system.
UserRouter.use(verifyInternalAdmin());

// --- ✅ LIST ALL USERS ---
UserRouter.get("/", async (ctx) => {
    try {
        // This server-to-server call fails with a 401 error
        const users = await auth.api.listUsers({ query: {} });
        return ctx.json({ data: users });
    } catch (error) {
        console.error("Error calling auth.api.listUsers:", error);
        // The error is logged here
        return ctx.json({ message: "Failed to list users" }, 500);
    }
});
  1. Make a request to this endpoint as an internal admin.

Expected Behavior

The server-to-server call from my backend to auth.api.listUsers should be authenticated and succeed, allowing my backend to perform administrative actions on Better Auth users.

Actual Behavior

The call throws an APIError with status: "UNAUTHORIZED" and statusCode: 401.

Error Log:

APIError:
     status: "UNAUTHORIZED",
       body: undefined,
    headers: {},
 statusCode: 401,

      at C:\...\node_modules\better-call\dist\index.js:56:5

Question & Feature Request

What is the recommended pattern for authenticating a backend application itself to perform these server-to-server admin operations?

The user who calls my API is authenticated by my own system and is irrelevant to Better Auth for this operation. I need my backend to be the authenticated entity.

  1. Is there a concept of a service account or a non-expiring API key that I can generate in Better Auth and configure in my backend's environment variables?
  2. If not, could this be considered a feature request? It's a common pattern for allowing a trusted backend to manage users programmatically.

This would allow my server to act as a trusted administrative client, which is essential for integrating Better Auth into an existing architecture with its own admin roles.

Thank you for the excellent library and for any guidance you can provide.

Originally created by @ayhamDev on GitHub (Aug 27, 2025). Hello, I am using `better-auth` as the user authentication and management service for my application. My primary backend is built with Hono.js and acts as a trusted subsystem. It's important to note that my backend has its **own, separate system for managing administrators**. The middleware I use (`verifyRole('admin')`) checks against my internal database, not against Better Auth roles. My goal is for my backend server to be able to make administrative calls to the Better Auth API (e.g., `listUsers`, `banUser`, `createUser`) as a trusted service. These actions are triggered by my internal admins using my backend's dashboard, but the call to Better Auth should be authenticated as the server itself. ### The Problem When my backend server attempts to call an admin-only method like `auth.api.listUsers()`, the request fails with a `401 Unauthorized` error. This is because the server-to-server call is being made without any authentication context. ### Steps to Reproduce 1. Configure a backend application (e.g., Hono, Express) with the `better-auth` client. 2. Create an API endpoint that is protected by an *internal* admin verification system (one that is separate from Better Auth). 3. Inside the endpoint's handler, make a call to an admin-only Better Auth method, like `auth.api.listUsers({ query: {} })`. **Example Code (Hono):** ```typescript // This middleware successfully verifies the user against our INTERNAL admin system. UserRouter.use(verifyInternalAdmin()); // --- ✅ LIST ALL USERS --- UserRouter.get("/", async (ctx) => { try { // This server-to-server call fails with a 401 error const users = await auth.api.listUsers({ query: {} }); return ctx.json({ data: users }); } catch (error) { console.error("Error calling auth.api.listUsers:", error); // The error is logged here return ctx.json({ message: "Failed to list users" }, 500); } }); ``` 4. Make a request to this endpoint as an internal admin. ### Expected Behavior The server-to-server call from my backend to `auth.api.listUsers` should be authenticated and succeed, allowing my backend to perform administrative actions on Better Auth users. ### Actual Behavior The call throws an `APIError` with `status: "UNAUTHORIZED"` and `statusCode: 401`. **Error Log:** ``` APIError: status: "UNAUTHORIZED", body: undefined, headers: {}, statusCode: 401, at C:\...\node_modules\better-call\dist\index.js:56:5 ``` ### Question & Feature Request What is the recommended pattern for authenticating a backend application itself to perform these server-to-server admin operations? The user who calls my API is authenticated by my own system and is irrelevant to Better Auth for this operation. I need my backend to be the authenticated entity. 1. Is there a concept of a **service account or a non-expiring API key** that I can generate in Better Auth and configure in my backend's environment variables? 2. If not, could this be considered a feature request? It's a common pattern for allowing a trusted backend to manage users programmatically. This would allow my server to act as a trusted administrative client, which is essential for integrating Better Auth into an existing architecture with its own admin roles. Thank you for the excellent library and for any guidance you can provide.
GiteaMirror added the staleenhancementhono labels 2026-03-13 09:03:30 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 27, 2025):

Better Auth supports secure server-to-server admin API calls using API keys, which work like service accounts. Here’s the recommended pattern:

  1. Enable the API key plugin in your Better Auth server config (details).
  2. Create an API key using the server-side auth.api.createApiKey method. You can set the userId (the user the key is associated with), permissions (as a JSON object for fine-grained access), and optionally configure the key to be non-expiring by setting defaultExpiresIn to null in the plugin options (schema & options).
  3. Store the API key securely (e.g., in environment variables). The full key is only returned at creation and is hashed in the database for security (security warning).
  4. Authenticate API calls by including the API key in the request headers (default: x-api-key). Your backend can then call admin endpoints like auth.api.listUsers with the API key in the headers (usage example).
  5. Configure permissions for the API key to control what admin actions it can perform. There’s no hardcoded “admin” role—privileges are managed via explicit permissions attached to the key (permissions details).

This setup allows your backend to act as a trusted admin client, independent of Better Auth’s user roles. If you need a non-expiring key, set defaultExpiresIn to null when configuring the plugin. For best security, rotate keys periodically and restrict permissions to only what’s needed.

If you need more granular examples or have questions about configuring permissions, let me know!

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 (Aug 27, 2025): <!-- Answer --> Better Auth supports secure server-to-server admin API calls using API keys, which work like service accounts. Here’s the recommended pattern: 1. **Enable the API key plugin** in your Better Auth server config ([details](https://github.com/better-auth/better-auth/issues/1815)). 2. **Create an API key** using the server-side `auth.api.createApiKey` method. You can set the `userId` (the user the key is associated with), permissions (as a JSON object for fine-grained access), and optionally configure the key to be non-expiring by setting `defaultExpiresIn` to `null` in the plugin options ([schema & options](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/types.ts)). 3. **Store the API key securely** (e.g., in environment variables). The full key is only returned at creation and is hashed in the database for security ([security warning](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/types.ts)). 4. **Authenticate API calls** by including the API key in the request headers (default: `x-api-key`). Your backend can then call admin endpoints like `auth.api.listUsers` with the API key in the headers ([usage example](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/api-key.test.ts)). 5. **Configure permissions** for the API key to control what admin actions it can perform. There’s no hardcoded “admin” role—privileges are managed via explicit permissions attached to the key ([permissions details](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/schema.ts)). This setup allows your backend to act as a trusted admin client, independent of Better Auth’s user roles. If you need a non-expiring key, set `defaultExpiresIn` to `null` when configuring the plugin. For best security, rotate keys periodically and restrict permissions to only what’s needed. If you need more granular examples or have questions about configuring permissions, let me know! <!-- 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/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ffb7d961-26f3-4bab-a1a0-a5e011544dd2?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/4257)
Author
Owner

@ayhamDev commented on GitHub (Aug 27, 2025):

@dosu
what if ther are on the same server , do i still need to create the api key ?
meaning its not server to server, but in the same server process i have the betterauth instance

@ayhamDev commented on GitHub (Aug 27, 2025): @dosu what if ther are on the same server , do i still need to create the api key ? meaning its not server to server, but in the same server process i have the betterauth instance
Author
Owner

@dosubot[bot] commented on GitHub (Aug 27, 2025):

If your backend and Better Auth are running in the same server process and you have access to the server-side auth instance, you can call privileged methods directly via the auth.api object. However, admin API endpoints still enforce authentication and permission checks at the API layer—even within the same process—so you need to provide an authentication context (like an API key in headers or a valid session) for admin actions. There’s no built-in bypass for authentication just because you’re in the same process—permissions are enforced the same way as over the network.

So yes, you still need to create and use an API key (or another valid authentication context) for admin API calls, even if everything is running in the same process. This ensures consistent security and permission enforcement across all usage patterns.
Reference

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 (Aug 27, 2025): <!-- Answer --> If your backend and Better Auth are running in the same server process and you have access to the server-side auth instance, you can call privileged methods directly via the `auth.api` object. However, admin API endpoints still enforce authentication and permission checks at the API layer—even within the same process—so you need to provide an authentication context (like an API key in headers or a valid session) for admin actions. There’s no built-in bypass for authentication just because you’re in the same process—permissions are enforced the same way as over the network. So yes, you still need to create and use an API key (or another valid authentication context) for admin API calls, even if everything is running in the same process. This ensures consistent security and permission enforcement across all usage patterns. [Reference](https://better-auth.com/docs/concepts/api) <!-- 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/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/38619a95-f966-4e97-976e-fc784c6d2524?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/4257)
Author
Owner

@ayhamDev commented on GitHub (Aug 27, 2025):

@dosu
but can any one create an api key ?
how will my server be able to be the only one to have admin privalges

@ayhamDev commented on GitHub (Aug 27, 2025): @dosu but can any one create an api key ? how will my server be able to be the only one to have admin privalges
Author
Owner

@dosubot[bot] commented on GitHub (Aug 27, 2025):

API key creation in Better Auth is restricted by authentication and permission checks. Only authenticated users (or server-side code providing a valid userId) can create API keys, and privileged properties (like permissions for admin actions) can only be set from server-side code—not from client-side requests. This means regular users or clients cannot create admin-level API keys or set sensitive properties—they’ll get an error if they try see implementation.

To ensure only your server can create admin API keys: use the server-side auth.api.createApiKey method, supply the userId of a trusted admin/service account, and set the permissions explicitly. You control who can create keys by restricting access to this method in your backend and by configuring Better Auth’s permission model so only designated users/entities have the ability to create API keys with admin privileges details.

Best practice: never expose API key creation to untrusted clients, and always store your admin API key securely (e.g., in environment variables). Review your Better Auth permission model to ensure only your backend (or trusted admin accounts) can create and use admin-level API keys.

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 (Aug 27, 2025): <!-- Answer --> API key creation in Better Auth is restricted by authentication and permission checks. Only authenticated users (or server-side code providing a valid userId) can create API keys, and privileged properties (like permissions for admin actions) can only be set from server-side code—not from client-side requests. This means regular users or clients cannot create admin-level API keys or set sensitive properties—they’ll get an error if they try [see implementation](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/routes/create-api-key.ts). To ensure only your server can create admin API keys: use the server-side `auth.api.createApiKey` method, supply the userId of a trusted admin/service account, and set the permissions explicitly. You control who can create keys by restricting access to this method in your backend and by configuring Better Auth’s permission model so only designated users/entities have the ability to create API keys with admin privileges [details](https://github.com/better-auth/better-auth/blob/f978fe9a983c336356364e2392ffcdd99e551b2b/packages/better-auth/src/plugins/api-key/index.ts). Best practice: never expose API key creation to untrusted clients, and always store your admin API key securely (e.g., in environment variables). Review your Better Auth permission model to ensure only your backend (or trusted admin accounts) can create and use admin-level API keys. <!-- 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/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8efc3c6c-3c11-4dbc-8b7b-e638d96ed740?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/4257)
Author
Owner

@benjaminpreiss commented on GitHub (Sep 20, 2025):

@dosu

how can I create an admin account then on a fresh better auth instance?

@benjaminpreiss commented on GitHub (Sep 20, 2025): @dosu how can I create an admin account then on a fresh better auth instance?
Author
Owner

@KlimentP commented on GitHub (Oct 2, 2025):

For anyone struggling with this, disregard the info from the dosubot, the api key plugin serves different purpose.

For backend server calls, just add headers to the request, for nextjs this would look like:
const users = await auth.api.listUsers({ query: {} }, { headers: await headers() });

@KlimentP commented on GitHub (Oct 2, 2025): For anyone struggling with this, disregard the info from the dosubot, the api key plugin serves different purpose. For backend server calls, just add headers to the request, for nextjs this would look like: `const users = await auth.api.listUsers({ query: {} }, { headers: await headers() });`
Author
Owner

@ayhamDev commented on GitHub (Oct 19, 2025):

@KlimentP
Is their away to be able to use the betterauth admin api without passing the headers ?
for example my api act as trusted source for the admin api, because i have somewhat of complicated situation where i cat use the betterauth admin role for admin requests, meaning i verify my own admins.

@ayhamDev commented on GitHub (Oct 19, 2025): @KlimentP Is their away to be able to use the betterauth admin api without passing the headers ? for example my api act as trusted source for the admin api, because i have somewhat of complicated situation where i cat use the betterauth admin role for admin requests, meaning i verify my own admins.
Author
Owner

@KlimentP commented on GitHub (Oct 20, 2025):

@ayhamDev, I am afraid not. Even if you verify on your server, by using the API, you are exposing endpoints that can be called from anywhere, not just your server, so you need a way to verify that whoever is calling the api has the necessary permission (admin role, user is in admin ids list, etc), otherwise you can run into all sorts of security issues. You can see in the docs: https://www.better-auth.com/docs/plugins/admin#list-users that the call to list-users and most of the admin endpoints requires passing the headers

@KlimentP commented on GitHub (Oct 20, 2025): @ayhamDev, I am afraid not. Even if you verify on your server, by using the API, you are exposing endpoints that can be called from anywhere, not just your server, so you need a way to verify that whoever is calling the api has the necessary permission (admin role, user is in admin ids list, etc), otherwise you can run into all sorts of security issues. You can see in the docs: https://www.better-auth.com/docs/plugins/admin#list-users that the call to list-users and most of the admin endpoints requires passing the headers
Author
Owner

@nkrkv commented on GitHub (Nov 3, 2025):

I had a similar problem. I need to seed some initial and demo data to make an environment re-creatable without clicking, starting servers, etc. I've found an approach based on things called "adapters". Probably they are internal details of Better Auth, but as of better-auth@npm:1.3.34, the following works:

/**
 * Universal seeding functions using Better Auth's internalAdapter.
 * This bypasses HTTP/auth layers and works directly with the database.
 */

import { auth } from "./auth"

export type Shop = {
  name: string
  slug: string
}

export type User = {
  email: string
  password: string
  name: string
  emailVerified?: boolean
}

export type MemberRole = "owner" | "admin" | "member"

export type SeedAction = "found" | "created" | "updated"

export type SeedResult<T> = {
  id: T
  action: SeedAction
}

/**
 * Seed a shop (organization).
 * @returns Shop ID and action (created or found)
 */
export async function seedShop(shop: Shop): Promise<SeedResult<string>> {
  const context = await auth.$context
  const adapter = context.adapter

  const existing = await adapter.findOne<{ id: string }>({
    model: "organization",
    where: [{ field: "slug", value: shop.slug }],
  })

  if (existing) {
    return { id: existing.id, action: "found" }
  }

  const orgEntity = await adapter.create({
    model: "organization",
    data: {
      name: shop.name,
      slug: shop.slug,
      createdAt: new Date(),
      updatedAt: new Date(),
    },
  })

  return { id: orgEntity.id, action: "created" }
}

/**
 * Seed a user with email/password credentials.
 * @returns User ID and action (created or found)
 */
export async function seedUser(user: User): Promise<SeedResult<string>> {
  const context = await auth.$context
  const internalAdapter = context.internalAdapter
  const passwordUtil = context.password

  const existingUser = await internalAdapter.findUserByEmail(user.email)

  if (existingUser) {
    return { id: existingUser.user.id, action: "found" }
  }

  const userEntity = await internalAdapter.createUser({
    email: user.email,
    emailVerified: user.emailVerified ?? true,
    name: user.name,
    createdAt: new Date(),
    updatedAt: new Date(),
  })

  const hashedPassword = await passwordUtil.hash(user.password)
  await internalAdapter.linkAccount({
    accountId: crypto.randomUUID(),
    providerId: "credential",
    password: hashedPassword,
    userId: userEntity.id,
  })

  return { id: userEntity.id, action: "created" }
}

/**
 * Seed a user membership to a shop with a specific role.
 * @returns Action (created or found)
 */
export async function seedUserToShop(
  userId: string,
  shopId: string,
  role: MemberRole
): Promise<SeedResult<void>> {
  const context = await auth.$context
  const adapter = context.adapter

  const existingMember = await adapter.findOne<{ id: string }>({
    model: "member",
    where: [
      { field: "userId", value: userId },
      { field: "organizationId", value: shopId },
    ],
  })

  if (existingMember) {
    return { id: undefined, action: "found" }
  }

  await adapter.create({
    model: "member",
    data: {
      organizationId: shopId,
      userId: userId,
      role: role,
      createdAt: new Date(),
    },
  })

  return { id: undefined, action: "created" }
}
@nkrkv commented on GitHub (Nov 3, 2025): I had a similar problem. I need to seed some initial and demo data to make an environment re-creatable without clicking, starting servers, etc. I've found an approach based on things called "adapters". Probably they are internal details of Better Auth, but as of `better-auth@npm:1.3.34`, the following works: ```typescript /** * Universal seeding functions using Better Auth's internalAdapter. * This bypasses HTTP/auth layers and works directly with the database. */ import { auth } from "./auth" export type Shop = { name: string slug: string } export type User = { email: string password: string name: string emailVerified?: boolean } export type MemberRole = "owner" | "admin" | "member" export type SeedAction = "found" | "created" | "updated" export type SeedResult<T> = { id: T action: SeedAction } /** * Seed a shop (organization). * @returns Shop ID and action (created or found) */ export async function seedShop(shop: Shop): Promise<SeedResult<string>> { const context = await auth.$context const adapter = context.adapter const existing = await adapter.findOne<{ id: string }>({ model: "organization", where: [{ field: "slug", value: shop.slug }], }) if (existing) { return { id: existing.id, action: "found" } } const orgEntity = await adapter.create({ model: "organization", data: { name: shop.name, slug: shop.slug, createdAt: new Date(), updatedAt: new Date(), }, }) return { id: orgEntity.id, action: "created" } } /** * Seed a user with email/password credentials. * @returns User ID and action (created or found) */ export async function seedUser(user: User): Promise<SeedResult<string>> { const context = await auth.$context const internalAdapter = context.internalAdapter const passwordUtil = context.password const existingUser = await internalAdapter.findUserByEmail(user.email) if (existingUser) { return { id: existingUser.user.id, action: "found" } } const userEntity = await internalAdapter.createUser({ email: user.email, emailVerified: user.emailVerified ?? true, name: user.name, createdAt: new Date(), updatedAt: new Date(), }) const hashedPassword = await passwordUtil.hash(user.password) await internalAdapter.linkAccount({ accountId: crypto.randomUUID(), providerId: "credential", password: hashedPassword, userId: userEntity.id, }) return { id: userEntity.id, action: "created" } } /** * Seed a user membership to a shop with a specific role. * @returns Action (created or found) */ export async function seedUserToShop( userId: string, shopId: string, role: MemberRole ): Promise<SeedResult<void>> { const context = await auth.$context const adapter = context.adapter const existingMember = await adapter.findOne<{ id: string }>({ model: "member", where: [ { field: "userId", value: userId }, { field: "organizationId", value: shopId }, ], }) if (existingMember) { return { id: undefined, action: "found" } } await adapter.create({ model: "member", data: { organizationId: shopId, userId: userId, role: role, createdAt: new Date(), }, }) return { id: undefined, action: "created" } } ```
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 3, 2025):

Hello, thanks for reaching out and sharing the details of your issue. Based on the information you provided, Better Auth doesn’t currently offer a built‐in service account or non-expiring API key mechanism for server-to-server calls. Instead, Better Auth is built around user session tokens, so typical admin actions require an authenticated user context.

A recommended pattern when you need trusted server-to-server operations—as illustrated in the seeding example—is to bypass the public HTTP/auth layers and use the internal adapter methods directly. This allows you to perform operations like creating or listing users without going through the external API endpoints that enforce the standard session checks [1].

If using the internal adapter directly is not suitable for your case, then this scenario falls under a potential feature request. Many community members have found a dedicated server-to-server authentication mechanism useful, so you might consider proposing this feature. I encourage you to tag @bekacru and @ping__ so they can provide further guidance or confirm any upcoming enhancements.

Are there any other aspects of your integration that you’d like to discuss or need further clarification on?
tag @better-auth-agent

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 3, 2025): Hello, thanks for reaching out and sharing the details of your issue. Based on the information you provided, Better Auth doesn’t currently offer a built‐in service account or non-expiring API key mechanism for server-to-server calls. Instead, Better Auth is built around user session tokens, so typical admin actions require an authenticated user context. A recommended pattern when you need trusted server-to-server operations—as illustrated in the seeding example—is to bypass the public HTTP/auth layers and use the internal adapter methods directly. This allows you to perform operations like creating or listing users without going through the external API endpoints that enforce the standard session checks [[1]](https://github.com/better-auth/better-auth/issues/4125). If using the internal adapter directly is not suitable for your case, then this scenario falls under a potential feature request. Many community members have found a dedicated server-to-server authentication mechanism useful, so you might consider proposing this feature. I encourage you to tag @bekacru and @ping__ so they can provide further guidance or confirm any upcoming enhancements. Are there any other aspects of your integration that you’d like to discuss or need further clarification on? tag @better-auth-agent <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Feb 2, 2026):

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

Issue Summary:

  • You encountered 401 errors when making server-to-server admin API calls due to missing authentication context.
  • Better Auth requires API keys with fine-grained permissions included in request headers for such calls, even within the same process.
  • API keys must be created by authenticated server-side code to maintain security.
  • For trusted internal operations, internal adapter methods can bypass HTTP/auth layers.
  • The team is open to feature requests for dedicated server-to-server authentication mechanisms.

Next Steps:

  • Please confirm if this issue is still relevant with the latest version of better-auth by commenting here.
  • If no response is received, I will automatically close this issue in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Feb 2, 2026): Hi, @ayhamDev. 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 encountered 401 errors when making server-to-server admin API calls due to missing authentication context. - Better Auth requires API keys with fine-grained permissions included in request headers for such calls, even within the same process. - API keys must be created by authenticated server-side code to maintain security. - For trusted internal operations, internal adapter methods can bypass HTTP/auth layers. - The team is open to feature requests for dedicated server-to-server authentication mechanisms. **Next Steps:** - Please confirm if this issue is still relevant with the latest version of better-auth by commenting here. - If no response is received, I will automatically close this issue in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1788