[GH-ISSUE #3323] Default user #9570

Closed
opened 2026-04-13 05:05:32 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @MrUprizing on GitHub (Jul 9, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3323

How can I create a default user?
For example from an environment variable.
My use case is for a project that is self-hosted and does not require user registration the default user would be the admin, and only from that admin’s console can users be created.

Originally created by @MrUprizing on GitHub (Jul 9, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3323 How can I create a default user? For example from an environment variable. My use case is for a project that is self-hosted and does not require user registration the default user would be the admin, and only from that admin’s console can users be created.
GiteaMirror added the locked label 2026-04-13 05:05:32 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 9, 2025):

Hi @MrUprizing! I'm Dosu and I’m helping the better-auth team.

There isn’t a built-in way in better-auth to seed a default user or admin from environment variables. The recommended approach is to write a custom initialization script that runs at startup, reads credentials from environment variables, and uses the better-auth adapter API (like internalAdapter.createUser) to create the admin user. You can then assign the admin role or add the user’s ID to the adminUserIds option in your admin plugin config for privileged access.

Here’s a rough outline:

  1. In your app’s startup logic (or a migration/init script), check if an admin user exists.
  2. If not, read the admin’s email, password, and name from environment variables.
  3. Use your adapter’s createUser method to create the user.
  4. Assign the admin role (either by setting the role field or adding the user’s ID to adminUserIds in your admin plugin config).

Example (pseudo-code):

const email = process.env.DEFAULT_ADMIN_EMAIL;
const password = process.env.DEFAULT_ADMIN_PASSWORD;
const name = process.env.DEFAULT_ADMIN_NAME || "Admin";

const existing = await internalAdapter.findUserByEmail(email);
if (!existing) {
  const user = await internalAdapter.createUser({ email, name, role: "admin" });
  const hashedPassword = await passwordHasher.hash(password);
  await internalAdapter.linkAccount({
    accountId: user.id,
    providerId: "credential",
    password: hashedPassword,
    userId: user.id,
  });
  // Optionally, add user.id to adminUserIds in your admin plugin config
}

For security, make sure your environment variables are set securely and never log sensitive values. Only run this script when you want to seed the initial admin.

References:

To reply, just mention @dosu.


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

<!-- gh-comment-id:3054170926 --> @dosubot[bot] commented on GitHub (Jul 9, 2025): <!-- Greeting --> Hi @MrUprizing! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> There isn’t a built-in way in better-auth to seed a default user or admin from environment variables. The recommended approach is to write a custom initialization script that runs at startup, reads credentials from environment variables, and uses the better-auth adapter API (like internalAdapter.createUser) to create the admin user. You can then assign the admin role or add the user’s ID to the adminUserIds option in your admin plugin config for privileged access. Here’s a rough outline: 1. In your app’s startup logic (or a migration/init script), check if an admin user exists. 2. If not, read the admin’s email, password, and name from environment variables. 3. Use your adapter’s createUser method to create the user. 4. Assign the admin role (either by setting the role field or adding the user’s ID to adminUserIds in your admin plugin config). Example (pseudo-code): ```ts const email = process.env.DEFAULT_ADMIN_EMAIL; const password = process.env.DEFAULT_ADMIN_PASSWORD; const name = process.env.DEFAULT_ADMIN_NAME || "Admin"; const existing = await internalAdapter.findUserByEmail(email); if (!existing) { const user = await internalAdapter.createUser({ email, name, role: "admin" }); const hashedPassword = await passwordHasher.hash(password); await internalAdapter.linkAccount({ accountId: user.id, providerId: "credential", password: hashedPassword, userId: user.id, }); // Optionally, add user.id to adminUserIds in your admin plugin config } ``` For security, make sure your environment variables are set securely and never log sensitive values. Only run this script when you want to seed the initial admin. References: - [Admin plugin roles and user creation](https://github.com/better-auth/better-auth/blob/b766b22865895f5cdd7dc575b0cc2e023e795740/packages/better-auth/src/plugins/admin/admin.ts) - [Admin plugin documentation](https://github.com/better-auth/better-auth/blob/b766b22865895f5cdd7dc575b0cc2e023e795740/docs/content/docs/plugins/admin.mdx) - [Hooks and adapter API](https://github.com/better-auth/better-auth/blob/b766b22865895f5cdd7dc575b0cc2e023e795740/docs/content/docs/concepts/hooks.mdx) <!-- 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/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ded50905-3bf9-4e00-8b09-675f7b796e89?feedback_type=other)</sup>&nbsp;&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/3323)
Author
Owner

@rebasecase commented on GitHub (Jul 10, 2025):

“How do I create a default credential?” Is that what you are asking?

I use a database seed for development locally. I read in the env var with the password, hash it using the same algorithm better-auth uses and insert the user into the relevant tables with the appropriate admin role.

For production you can seed the database with these data in an initial migration. Or, indeed, do what the bot says at app startup.

<!-- gh-comment-id:3056099286 --> @rebasecase commented on GitHub (Jul 10, 2025): “How do I create a default credential?” Is that what you are asking? I use a database seed for development locally. I read in the env var with the password, hash it using the same algorithm better-auth uses and insert the user into the relevant tables with the appropriate admin role. For production you can seed the database with these data in an initial migration. Or, indeed, do what the bot says at app startup.
Author
Owner

@ping-maxwell commented on GitHub (Jul 10, 2025):

Something simple like this is what I usually do:

import { auth } from "./src/lib/auth";

try {
	const res = await auth.api.signUpEmail({
		body: {
			name: "test123",
			email: "test123@test.com",
			password: "test123@test.com",
		},
	});
	console.log("Sign up successful:", res);
} catch (error) {
	console.log("Sign up failed:", error);
}

Just put that in a seed.ts and away you go. Just run that with bun and it will create the user.

In your case I would then just use your ORM to update that user's role to be admin (I assume you're using the admin plugin)

<!-- gh-comment-id:3058291235 --> @ping-maxwell commented on GitHub (Jul 10, 2025): Something simple like this is what I usually do: ```ts import { auth } from "./src/lib/auth"; try { const res = await auth.api.signUpEmail({ body: { name: "test123", email: "test123@test.com", password: "test123@test.com", }, }); console.log("Sign up successful:", res); } catch (error) { console.log("Sign up failed:", error); } ``` Just put that in a `seed.ts` and away you go. Just run that with bun and it will create the user. In your case I would then just use your ORM to update that user's role to be `admin` (I assume you're using the admin plugin)
Author
Owner

@adriangalilea commented on GitHub (Jul 15, 2025):

I believe this material belongs to the docs.

“Seeding the database with your first user”

It is something I figured out, but then I forgot and tried searching again on the docs only to not find it and arrive here.

<!-- gh-comment-id:3073392825 --> @adriangalilea commented on GitHub (Jul 15, 2025): I believe this material belongs to the docs. “Seeding the database with your first user” It is something I figured out, but then I forgot and tried searching again on the docs only to not find it and arrive here.
Author
Owner

@airtonix commented on GitHub (Sep 26, 2025):

This have changed a bit (or maybe I'm doing it wrong)

import { buildCommand } from "@stricli/core";

import { getAuthService } from "../../services/auth";
import { whenReady } from "../../services/db";
import { logger } from "../../services/logger";

const log = logger.extend("cmd:db:init");


type AuthService = Awaited<ReturnType<typeof getAuthService>>;
type UserToCreate = Parameters<AuthService["api"]["createUser"]>[0]["body"];

const users: UserToCreate[] = [
   //...
]

export const initialiseCommand = buildCommand({
  func: async () => {
    log.info("AuthService.creating");
    await whenReady({ maxAttempts: 5, delaySeconds: 2 });
    const authService = await getAuthService();
    const internalAdapter = (await authService.$context).internalAdapter;
    const password = (await authService.$context).password;
    log.info("AuthService.created");
    for (const userData of users) {
      const exists = await internalAdapter.findUserByEmail(userData.email);

      if (exists) {
        log.info("User.exists", { email: userData.email });
        continue;
      }

      log.info("User.create", userData);
      const user = await internalAdapter.createUser({
        email: userData.email,
        emailVerified: true,
        role: userData.role,
        name: userData.name,
        createdAt: new Date(),
        updatedAt: new Date(),
      });

      await internalAdapter.linkAccount({
        accountId: user.id,
        providerId: "credential",
        password: await password.hash(userData.password),
        userId: user.id,
      });

      log.info("User.created", { email: user.email });
    }
    process.exit(0);
  },
  docs: {
    brief: "Initialise the database with default data",
  },
  parameters: {},
});
<!-- gh-comment-id:3339106259 --> @airtonix commented on GitHub (Sep 26, 2025): This have changed a bit (or maybe I'm doing it wrong) ```ts import { buildCommand } from "@stricli/core"; import { getAuthService } from "../../services/auth"; import { whenReady } from "../../services/db"; import { logger } from "../../services/logger"; const log = logger.extend("cmd:db:init"); type AuthService = Awaited<ReturnType<typeof getAuthService>>; type UserToCreate = Parameters<AuthService["api"]["createUser"]>[0]["body"]; const users: UserToCreate[] = [ //... ] export const initialiseCommand = buildCommand({ func: async () => { log.info("AuthService.creating"); await whenReady({ maxAttempts: 5, delaySeconds: 2 }); const authService = await getAuthService(); const internalAdapter = (await authService.$context).internalAdapter; const password = (await authService.$context).password; log.info("AuthService.created"); for (const userData of users) { const exists = await internalAdapter.findUserByEmail(userData.email); if (exists) { log.info("User.exists", { email: userData.email }); continue; } log.info("User.create", userData); const user = await internalAdapter.createUser({ email: userData.email, emailVerified: true, role: userData.role, name: userData.name, createdAt: new Date(), updatedAt: new Date(), }); await internalAdapter.linkAccount({ accountId: user.id, providerId: "credential", password: await password.hash(userData.password), userId: user.id, }); log.info("User.created", { email: user.email }); } process.exit(0); }, docs: { brief: "Initialise the database with default data", }, parameters: {}, }); ```
Author
Owner

@sq-tupai commented on GitHub (Dec 3, 2025):

My simple implementation,

  databaseHooks: {
    user: {
      create: {
        async before(user) {
          return {
            data: {
              ...user,
              role:
                process.env.ADMIN_EMAIL === user.email ? "admin" : "user",
            },
          };
        },
      },
    },
  },
<!-- gh-comment-id:3606329212 --> @sq-tupai commented on GitHub (Dec 3, 2025): My simple implementation, ``` databaseHooks: { user: { create: { async before(user) { return { data: { ...user, role: process.env.ADMIN_EMAIL === user.email ? "admin" : "user", }, }; }, }, }, }, ```
Author
Owner

@bfourgeaud commented on GitHub (Dec 9, 2025):

For information what works with Prisma :

import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"

export const users = [
  {
    email: "john.doe@domain.com",
    name: "John Doe",
    role: "admin",
  },
] as const

export async function seedUsers() {
  console.log("🌱 Seeding users...")

  const password = process.env.DEFAULT_PASSWORD
  if (!password) throw new Error("DEFAULT_PASSWORD is not defined")

  const operations = users.map((user) =>
    auth.api.createUser({
      body: {
        name: user.name,
        email: user.email,
        password,
        role: user.role,
      },
    })
  )

  const results = await Promise.allSettled(operations)

  const created = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value.user)

  // Updating created users to have verified email
  await prisma.user.updateMany({
    where: {
      id: { in: created.map(({ id }) => id) },
    },
    data: { emailVerified: true },
  })

  console.log(`✅ ${created.length} Users seeded`)
}

<!-- gh-comment-id:3633776518 --> @bfourgeaud commented on GitHub (Dec 9, 2025): For information what works with Prisma : ```ts import { auth } from "@/lib/auth" import { prisma } from "@/lib/prisma" export const users = [ { email: "john.doe@domain.com", name: "John Doe", role: "admin", }, ] as const export async function seedUsers() { console.log("🌱 Seeding users...") const password = process.env.DEFAULT_PASSWORD if (!password) throw new Error("DEFAULT_PASSWORD is not defined") const operations = users.map((user) => auth.api.createUser({ body: { name: user.name, email: user.email, password, role: user.role, }, }) ) const results = await Promise.allSettled(operations) const created = results .filter((r) => r.status === "fulfilled") .map((r) => r.value.user) // Updating created users to have verified email await prisma.user.updateMany({ where: { id: { in: created.map(({ id }) => id) }, }, data: { emailVerified: true }, }) console.log(`✅ ${created.length} Users seeded`) } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9570