Account Deletion Policies

Customize account deletion authorization rules.

Account deletion policies are authorization hooks that run before a user can delete their account. Use them to enforce business rules - like requiring subscription cancellation or data export - or to perform cleanup tasks before deletion proceeds. The system ships with sensible defaults (block if active subscriptions, block if sole org owner) that you can extend, replace, or cherry-pick.

Account deletion policies are pluggable authorization rules that evaluate whether a user can delete their account, returning allow/deny with optional remediation instructions.

  • Use policies when: you need to enforce business rules before account deletion (subscription status, data retention, ownership transfer).
  • Extend defaults when: the built-in subscription and organization checks are correct but you need additional rules.
  • Replace policies when: your business logic differs significantly from the defaults (e.g., auto-cancel subscriptions instead of blocking).

Default Policies

The @kit/account-hooks package includes two default policies:

PolicyIDBehavior
checkSubscriptionsPolicyaccount-deletion.check-subscriptionsBlocks if user has active subscriptions
checkOrganizationsPolicyaccount-deletion.check-organizationsBlocks if sole owner of orgs with other members

Adding Custom Policies

1. Create Your Policy

// In your app or a custom package
import { definePolicy, allow, deny } from '@kit/policies';
import type { AccountDeleteContext } from '@kit/account-hooks';
export const gracePeriodPolicy = definePolicy<AccountDeleteContext>({
id: 'account-deletion.grace-period',
evaluate: async (context) => {
const user = await getUser(context.userId);
const daysSinceCreation = daysBetween(user.createdAt, new Date());
if (daysSinceCreation < 7) {
return deny({
code: 'ACCOUNT_TOO_NEW',
message: 'Accounts must be at least 7 days old before deletion',
remediation: `Please wait ${7 - daysSinceCreation} more days`,
});
}
return allow();
},
});

2. Register Your Policy

Extend the default registry:

import { accountDeletionRegistry } from '@kit/account-hooks';
import { gracePeriodPolicy } from './my-policies';
// Add to existing defaults
accountDeletionRegistry.registerPolicy(gracePeriodPolicy);

Context Type

interface AccountDeleteContext {
userId: string; // User attempting to delete account
timestamp: string; // ISO timestamp
}

Customization Patterns

Use Defaults As-Is

import { accountDeletion } from '@kit/account-hooks';
const result = await accountDeletion.run({
userId: user.id,
timestamp: new Date().toISOString(),
});

Extend Defaults

import { accountDeletionRegistry } from '@kit/account-hooks';
import { myCustomPolicy } from './policies';
accountDeletionRegistry.registerPolicy(myCustomPolicy);

Cherry-Pick Policies

import { createPolicyRegistry, createPolicyRuntime } from '@kit/policies';
import { checkOrganizationsPolicy } from '@kit/account-hooks';
const registry = createPolicyRegistry();
// Only check organizations, skip subscriptions
registry.registerPolicy(checkOrganizationsPolicy);
const runtime = createPolicyRuntime(registry);

Replace a Policy

import { createPolicyRegistry, createPolicyRuntime } from '@kit/policies';
import { checkOrganizationsPolicy } from '@kit/account-hooks';
import { mySubscriptionPolicy } from './policies';
const registry = createPolicyRegistry();
registry.registerPolicy(checkOrganizationsPolicy);
registry.registerPolicy(mySubscriptionPolicy); // Your replacement
const runtime = createPolicyRuntime(registry);

Example: Auto-Cancel Subscriptions

Instead of blocking deletion, auto-cancel subscriptions:

import { definePolicy, allow } from '@kit/policies';
export const autoCancelPolicy = definePolicy<AccountDeleteContext>({
id: 'account-deletion.auto-cancel-subscriptions',
evaluate: async (context) => {
const subscriptions = await getActiveSubscriptions(context.userId);
for (const sub of subscriptions) {
await cancelSubscription(sub.id);
}
return allow({
cancelledSubscriptions: subscriptions.length,
});
},
});

Example: Require Data Export

export const dataExportPolicy = definePolicy<AccountDeleteContext>({
id: 'account-deletion.require-export',
evaluate: async (context) => {
const hasExported = await checkDataExportStatus(context.userId);
if (!hasExported) {
return deny({
code: 'DATA_NOT_EXPORTED',
message: 'Please export your data before deleting your account',
remediation: 'Go to Settings > Privacy > Export Data',
});
}
return allow();
},
});

Integration

The policies are evaluated in the Better Auth beforeDeleteUser hook:

// packages/account/hooks/src/before-delete.ts
import { accountDeletion } from './policies/registry';
export async function beforeAccountDelete(userId: string) {
const result = await accountDeletion.run({
userId,
timestamp: new Date().toISOString(),
});
if (!result.allowed) {
throw new Error('Account deletion blocked');
}
}

Common Pitfalls

  • Policy order matters: Policies run in registration order. Put blocking checks (subscriptions, ownership) before transformations (auto-cancel, data export). A failed early policy short-circuits later ones.
  • Async policies that throw don't roll back: If a policy throws an exception mid-execution, previous policy side effects (like canceling a subscription) aren't reversed. Handle errors gracefully with deny().
  • Remediation messages not shown to users: The remediation field in deny() should contain user-facing instructions. Don't put technical error codes here - tell users what to do.
  • Testing in production: Failed deletion policies can leave accounts in limbo. Always test policy combinations in development with real scenarios (active subscription + sole org owner).
  • Forgetting to register custom policies: Creating a policy isn't enough - call registry.registerPolicy() before the runtime is used. Registration typically happens in a setup module.

Frequently Asked Questions

Can I remove the default subscription check policy?
Yes. Create a new registry with createPolicyRegistry() and only register the policies you want. Don't use the default accountDeletionRegistry if you want full control.
How do I show policy denial reasons to users?
The deny() function accepts a remediation field with user-facing instructions. The UI component reads this from the policy result and displays it to users.
Do policies run in a transaction?
No. Each policy runs independently. If a later policy fails, earlier policy side effects (like canceling subscriptions) are not rolled back. Design policies to be idempotent.
Can I run policies asynchronously?
Policies are async by default. The evaluate function returns a Promise. All policies run sequentially in registration order - not in parallel.
How do I test policies in development?
Create test users with different states (active subscription, sole org owner, etc.) and attempt account deletion. Check that the correct denial messages appear.

Next: Organizations →