[GH-ISSUE #8621] Built-in "deleted accounts" blocklist to prevent re-signup abuse #11141

Open
opened 2026-04-13 07:30:29 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @vishalkadam47 on GitHub (Mar 15, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8621

Is this suited for github?

  • Yes, this is suited for github

No response

Describe the solution you'd like

When a user deletes their account, there's currently no built-in mechanism to prevent them from immediately re-signing up with the same email (e.g. via OAuth) and getting fresh free credits/trial access again.

Describe alternatives you've considered

Add an optional user.deleteUser.blockReSignup config that, when enabled:

  • Stores the deleted email in a deleted_accounts table (or similar)
  • Checks that table in databaseHooks.user.create.before and blocks re-creation
  • Surfaces a proper APIError with a meaningful error code like ACCOUNT_PERMANENTLY_DELETED so the OAuth error callback can display a friendly message

Any SaaS with free tiers is vulnerable to abuse via repeated account creation.

Additional context

No response

Originally created by @vishalkadam47 on GitHub (Mar 15, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8621 ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. _No response_ ### Describe the solution you'd like When a user deletes their account, there's currently no built-in mechanism to prevent them from immediately re-signing up with the same email (e.g. via OAuth) and getting fresh free credits/trial access again. ### Describe alternatives you've considered Add an optional `user.deleteUser.blockReSignup` config that, when enabled: - Stores the deleted email in a `deleted_accounts` table (or similar) - Checks that table in `databaseHooks.user.create.before` and blocks re-creation - Surfaces a proper `APIError` with a meaningful error code like `ACCOUNT_PERMANENTLY_DELETED` so the OAuth error callback can display a friendly message Any SaaS with free tiers is vulnerable to abuse via repeated account creation. ### Additional context _No response_
GiteaMirror added the credentialssecurity labels 2026-04-13 07:30:29 -05:00
Author
Owner

@vishalkadam47 commented on GitHub (Mar 15, 2026):

here's the workaround I'm currently using:

Add a deleted_accounts table to your schema:

export const deletedAccounts = pgTable("deleted_accounts", {
  email: text("email").primaryKey(),
  deletedAt: timestamp("deleted_at").notNull().defaultNow(),
});

Block re-signup in databaseHooks.user.create.before :

databaseHooks: {
  user: {
    create: {
      before: async (user) => {
        const [blocked] = await db
          .select()
          .from(deletedAccounts)
          .where(eq(deletedAccounts.email, user.email))
          .limit(1);

        if (blocked) {
          throw new APIError("FORBIDDEN", {
            message: "This account has been permanently deleted and cannot be re-created.",
            code: "ACCOUNT_PERMANENTLY_DELETED",
          });
        }
        return { data: user };
      },
    },
  },
},

Blocklist the email on delete in afterDelete :

user: {
  deleteUser: {
    enabled: true,
    afterDelete: async (user) => {
      await db.insert(deletedAccounts).values({ email: user.email }).onConflictDoNothing();
    },
  },
},

On the frontend, read the error query param (Better Auth redirects back with the message URL-encoded) and show a friendly message.

It works but it's a lot of boilerplate that would be much cleaner as a built-in deleteUser.preventReSignup

<!-- gh-comment-id:4064047559 --> @vishalkadam47 commented on GitHub (Mar 15, 2026): here's the workaround I'm currently using: Add a `deleted_accounts` table to your schema: ```ts export const deletedAccounts = pgTable("deleted_accounts", { email: text("email").primaryKey(), deletedAt: timestamp("deleted_at").notNull().defaultNow(), }); ``` Block re-signup in `databaseHooks.user.create.before` : ```ts databaseHooks: { user: { create: { before: async (user) => { const [blocked] = await db .select() .from(deletedAccounts) .where(eq(deletedAccounts.email, user.email)) .limit(1); if (blocked) { throw new APIError("FORBIDDEN", { message: "This account has been permanently deleted and cannot be re-created.", code: "ACCOUNT_PERMANENTLY_DELETED", }); } return { data: user }; }, }, }, }, ``` Blocklist the email on delete in `afterDelete` : ```ts user: { deleteUser: { enabled: true, afterDelete: async (user) => { await db.insert(deletedAccounts).values({ email: user.email }).onConflictDoNothing(); }, }, }, ``` On the frontend, read the error query param (Better Auth redirects back with the message URL-encoded) and show a friendly message. It works but it's a lot of boilerplate that would be much cleaner as a built-in `deleteUser.preventReSignup`
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11141