@forgehustle/better-auth-soft-deletion
v1.1.1
Published
Production-grade soft deletion plugin for Better Auth
Maintainers
Readme
@forgehustle/better-auth-soft-deletion
Soft deletion plugin for Better Auth with:
- user soft delete (
status = "deleted",deletedAt) - sign-in blocking for deleted users
- optional re-registration blocking during retention window
- account restore endpoint
- immediate revocation of all user sessions on delete
What this plugin does
When a user deletes their account:
- User row is kept (not hard-deleted)
statusbecomes"deleted"anddeletedAtis set- All active sessions are revoked
- (Optional) Email is blocked from re-registering for
retentionDays
When a deleted user tries to sign in:
- plugin returns
403 FORBIDDENwith codeACCOUNT_DELETED
When restore is called with valid email/password:
- user status is set back to
"active" deletedAtis cleared- blocked identifier entry is removed
Restore endpoint exposed by this plugin:
POST /soft-deletion/restore
Installation
npm install @forgehustle/better-auth-soft-deletion
# or
bun add @forgehustle/better-auth-soft-deletionServer setup (Better Auth)
import { betterAuth } from "better-auth";
import { softDeletion } from "@forgehustle/better-auth-soft-deletion";
export const auth = betterAuth({
// your adapter
// database: drizzleAdapter(...)
plugins: [
softDeletion({
retentionDays: 30, // default: 30
blockReRegistration: true, // default: true
}),
],
user: {
deleteUser: {
enabled: true,
},
},
});Important:
deleteUsermust be enabled in Better Auth config.- your client should send password confirmation to delete account (
passwordstring).
Client setup
import { createAuthClient } from "better-auth/react";
import { softDeletionClient } from "@forgehustle/better-auth-soft-deletion/client";
export const authClient = createAuthClient({
baseURL: "http://localhost:5000/auth", // adjust for your app
plugins: [softDeletionClient()],
});Important:
- Prefer plugin actions from
authClientfor restore operations. - Avoid manual
fetchcalls to plugin endpoints in React apps.
Usage examples
Delete account (requires password confirmation)
await authClient.deleteUser({
password: "CurrentPassword123!",
});If password is missing/invalid shape, Better Auth will reject the request.
Restore account (recommended)
const { data, error } = await authClient.restoreAccount({
email: "[email protected]",
password: "CurrentPassword123!",
});
if (error) {
// handle by error.code
console.error(error.code, error.message);
} else {
console.log(data.message); // "Account restored successfully."
}Options
type SoftDeletionOptions = {
retentionDays?: number; // default: 30
blockReRegistration?: boolean; // default: true
restoreRateLimit?: (
params: { email: string; context: unknown }
) => boolean | { allowed: boolean; code?: string; message?: string; status?: number };
};Added schema
This plugin extends Better Auth schema with:
user.status(string, default:"active")user.deletedAt(date | null)blockedIdentifiermodel:identifierHashtypeexpiresAt
Run your Better Auth/ORM migration flow after enabling plugin schema changes.
Error codes you should handle on client
ACCOUNT_DELETED(403): deleted user attempted sign-inEMAIL_BLOCKED(403): re-registration blocked during retention windowRESTORE_INPUT_REQUIRED(400): email/password missing on restoreACCOUNT_NOT_DELETED(400): restore requested for active accountNO_PASSWORD_CREDENTIAL(400): credential password not available (for example OAuth-only account)AUTH_INVALID_CREDENTIALS(401): invalid email/password on restore
Security notes
- Always send restore credentials in POST JSON body.
- Do not put email/password in query string.
- Revoke sessions on delete is already built in:
- uses Better Auth internal adapter
deleteSessions(userId)when available - fallback removes
sessionmodel rows byuserId
- uses Better Auth internal adapter
- If your app uses Better Auth
session.cookieCache, revoked sessions may appear active until cache refresh on other devices. For strict behavior, disable cookie cache or forcedisableCookieCache: trueon session refresh checks.
Troubleshooting
Password confirmation is required to delete your account:- ensure
deleteUser({ password: "..." })is sent withpasswordas string.
- ensure
VALIDATION_ERROR [body.password] expected string, received object:- your payload shape is wrong. Send a plain string value for
password.
- your payload shape is wrong. Send a plain string value for
RESTORE_INPUT_REQUIRED:- restore request is missing
emailorpassword. - verify request body is JSON and keys are exact:
email,password.
- restore request is missing
Deleted user can still access protected routes on another browser:
- this is usually session cookie cache behavior, not DB/Redis session persistence.
- disable Better Auth session cookie cache for strict immediate invalidation.
License
MIT
