21 KiB
Profile Deletion Grace Period
This document describes the grace period system for profile deletions, which allows profiles to be restored within a configurable timeframe after deletion.
Overview
When a profile is deleted (either manually by the user or automatically due to inactivity), the system implements a two-phase deletion process:
- Soft Deletion: Profile is marked as deleted but data remains intact
- Permanent Deletion: After the grace period expires, profile data is permanently anonymized and balances are handled
This gives users a safety window to restore their profiles if deleted accidentally or if they change their mind.
Configuration
The grace period is configured per platform in the timebank configuration files:
Location: config/timebank-*.php
'delete_profile' => [
'grace_period_days' => 30, // Number of days before permanent deletion
// ... other deletion settings
],
Default Value: 30 days
Recommendation: Keep at least 14 days to give users adequate time to restore their profiles.
How It Works
Manual Deletion (User-Initiated)
- User initiates deletion from profile settings
- Profile is soft-deleted:
deleted_attimestamp is set to current time- Balance handling preferences are stored in Redis cache (key:
balance_handling_{ModelClass}_{ID}) - Cache TTL: grace period + 7 days buffer
commentfield remains empty for manual deletions- User is logged out and redirected to goodbye page
- Confirmation email is sent with grace period information
- Profile can be restored during grace period
- After grace period expires, scheduled command permanently deletes the profile
Auto-Deletion (Inactivity-Based)
- System detects inactive profile (based on
inactive_attimestamp) - Warning emails are sent before deletion
- Profile is soft-deleted if user doesn't respond
- Profile is soft-deleted:
deleted_attimestamp is set to current time- Balance handling preferences are stored in Redis cache
commentfield is set to: "Profile automatically deleted after X days of inactivity." (translated)
- Email notification sent about automatic deletion
- Grace period begins, allowing restoration
- After grace period expires, permanent deletion occurs
Permanent Deletion Process
After the grace period expires:
-
Balance handling preferences are retrieved from Redis cache:
- Cache key:
balance_handling_{ModelClass}_{ID} - If cache found: Execute user's choice (donate or destroy currency)
- FALLBACK: If cache lost, destroy currency (transfer to debit account)
- Fallback triggers a warning log for monitoring
- Cache key:
-
Balance handling is executed:
- If donated: transfers to selected organization account
- If deleted (or fallback): balances transferred to debit account (currency removed from circulation)
-
Profile data is anonymized:
- Email:
removed-{id}@remove.ed - Name:
Removed {profiletype} {id} - Password: randomized
- Personal data: cleared
- Comment: cleared
- Email:
-
Profile photo deleted
-
Related data cleaned up (stars, bookmarks, etc.)
-
Cache is cleared after successful completion
Artisan Commands
Restore Deleted Profile
Restore a profile that was deleted within the grace period.
Command Signature
php artisan profiles:restore {username?} {--list} {--type=}
Parameters
{username?}- Optional. Username of the profile to restore--list- Display all profiles eligible for restoration--type=- Filter by profile type (user, organization, bank, admin)
Usage Examples
List all restorable profiles:
php artisan profiles:restore --list
List restorable profiles by type:
php artisan profiles:restore --list --type=user
php artisan profiles:restore --list --type=organization
Restore specific profile:
php artisan profiles:restore john_doe
Interactive mode (prompts for username):
php artisan profiles:restore
Output Example
Available profiles for restoration:
Users:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email │ Deleted │ Time Left │
├──────────┼───────────────┼──────────────┼─────────────┤
│ john_doe │ john@test.com │ 2 days ago │ 28 days │
│ jane_smith│ jane@test.com │ 5 hours ago │ 29 days │
└──────────┴───────────────┴──────────────┴─────────────┘
Organizations:
┌──────────┬───────────────┬──────────────┬─────────────┐
│ Username │ Email │ Deleted │ Time Left │
├──────────┼───────────────┼──────────────┼─────────────┤
│ org_test │ org@test.com │ 1 day ago │ 29 days │
└──────────┴───────────────┴──────────────┴─────────────┘
Total: 3 profile(s) can be restored
Restoration Process
The command will:
- Verify the profile exists and is within grace period
- Check that the profile hasn't been anonymized
- Clear the
deleted_attimestamp - Clear stored balance handling preferences from
commentfield - Log the restoration action
- Display success message with profile details
Error Handling
The command handles various error scenarios:
- Profile not found: "Profile 'username' not found"
- Already active: "Profile 'username' is not deleted"
- Grace period expired: "Profile 'username' cannot be restored (grace period expired)"
- Already anonymized: "Profile 'username' has already been permanently deleted"
Permanent Deletion Command (Scheduled)
This command runs automatically via Laravel scheduler to permanently delete expired profiles.
Command Signature
php artisan profiles:permanently-delete-expired
Schedule Configuration
Location: app/Console/Kernel.php
$schedule->command('profiles:permanently-delete-expired')
->daily()
->at('02:30')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/permanent-deletions.log'));
Default Schedule: Daily at 02:30 AM
What It Does
- Finds all profiles where:
deleted_atis setdeleted_at+ grace_period_days < now- Email is not already anonymized (not deleted_user_*)
- For each expired profile:
- Retrieves balance handling preferences from
commentfield - Calls
DeleteUser::permanentlyDelete()method - Handles balances according to stored preferences
- Anonymizes profile data
- Retrieves balance handling preferences from
- Logs all actions to
storage/logs/permanent-deletions.log
Manual Execution
You can manually trigger permanent deletion of expired profiles:
php artisan profiles:permanently-delete-expired
Warning: This command is destructive and cannot be undone. Only run manually for testing or if you need to immediately process expired profiles.
Email Notifications
User-Deleted Email
Sent immediately when a profile is deleted.
Template: resources/views/emails/administration/user-deleted.blade.php
Contains:
- Account details (username, email, deletion time)
- Balance handling information (if applicable)
- Grace period notification
- Data handling information
Key Message:
"This action will become permanent after {days} days."
Auto-Deleted Email
Sent when a profile is automatically deleted due to inactivity.
Template: Same as user-deleted, with additional fields:
autoDeleted: true- Deletion reason
- Days of inactivity
Technical Implementation
Data Storage During Grace Period
Profile Models (User, Organization, Bank, Admin):
deleted_at(datetime): Timestamp when profile was soft-deletedcomment(text):- Auto-deletion: Human-readable message (e.g., "Profile automatically deleted after 120 days of inactivity.")
- Manual deletion: Empty (no comment)
Balance Handling Cache Storage (Redis):
Why Cache?
- Balance handling is stored in cache (not database) to keep the
commentfield human-readable for administrators - User's balance handling choice (donate vs delete) needs to persist through the grace period
- Balances are handled during permanent deletion (after grace period expires), not during soft delete
- Cache automatically expires after grace period + buffer, self-cleaning the data
- Transactions table provides permanent audit trail of actual balance transfers
Cache Details:
- Key:
balance_handling_{ModelClass}_{ProfileID} - TTL: Grace period days + 7 days buffer
- Structure:
[ 'option' => 'donate|delete', 'donation_account_id' => 123, // null if option is 'delete' 'stored_at' => '2025-12-19 10:30:00' ]
Cache Fallback:
- If cache is lost during permanent deletion, the system defaults to destroying currency (transfer to debit account)
- A warning is logged:
Balance handling cache lost for profile deletion - This ensures no orphaned balances remain in the system
- Transaction table will show the fallback action for audit purposes
Translation Keys
Auto-deletion comment (resources/lang/*.json):
- English:
"Profile automatically deleted after :days days of inactivity." - Dutch:
"Profiel automatisch verwijderd na :days dagen inactiviteit." - French:
"Profil automatiquement supprimé après :days jours d'inactivité." - Spanish:
"Perfil eliminado automáticamente después de :days días de inactividad." - German:
"Profil automatisch nach :days Tagen Inaktivität gelöscht."
Key Classes and Files
Actions
app/Actions/Jetstream/DeleteUser.php
delete(): Soft deletes profile, stores balance preferences in Redis cachepermanentlyDelete(): Retrieves balance preferences from cache, handles balances, anonymizes profile, clears cache
app/Actions/Jetstream/RestoreProfile.php
restore(): Restores a soft-deleted profile within grace period
Commands
app/Console/Commands/RestoreDeletedProfile.php
- Interactive CLI for restoring profiles
- Lists eligible profiles
- Handles restoration process
app/Console/Commands/PermanentlyDeleteExpiredProfiles.php
- Scheduled task for permanent deletion
- Processes expired profiles
- Logs all actions
Livewire Components
app/Http/Livewire/Profile/DeleteUserForm.php
deleteUser(): Handles user-initiated deletion- Stores balance handling options
- Manages logout and session
- Redirects to goodbye page
Views
resources/views/goodbye-deleted-user.blade.php
- Displayed after manual deletion
- Shows deletion confirmation and grace period info
Routes
Route: /goodbye-deleted-user (Named: goodbye-deleted-user)
- Displays goodbye page after deletion
- Requires
session('result')data
Testing
Test Manual Deletion
-
Set grace period to 1 day in config for testing:
'grace_period_days' => 1, -
Delete a test profile from profile settings
-
Verify:
- Goodbye page displays
- Email received with grace period info
- Profile listed in
php artisan profiles:restore --list
-
Restore the profile:
php artisan profiles:restore test_user -
Verify profile is active and can log in
Test Automatic Deletion
-
Run the permanent deletion command after grace period:
php artisan profiles:permanently-delete-expired -
Verify:
- Profile is permanently deleted
- Data is anonymized
- Balances handled according to preferences
- Logged in
storage/logs/permanent-deletions.log
Test Edge Cases
Grace period expired:
php artisan profiles:restore expired_user
# Should show: "Profile cannot be restored (grace period expired)"
Already anonymized:
php artisan profiles:restore permanently_deleted_user
# Should show: "Profile has already been permanently deleted"
Non-existent profile:
php artisan profiles:restore nonexistent
# Should show: "Profile 'nonexistent' not found"
Best Practices
For Administrators
-
Monitor grace period activity:
- Check
storage/logs/permanent-deletions.logregularly - Review restoration requests
- Adjust grace period based on user feedback
- Check
-
Backup before major changes:
- Database backup before reducing grace period
- Test restoration process in staging first
-
User communication:
- Clearly communicate grace period in deletion UI
- Include grace period in Terms of Service
- Send reminder emails before permanent deletion (optional enhancement)
For Developers
-
Never bypass grace period:
- Always use
DeleteUser@delete()for deletions - Never directly set
deleted_atwithout storing balance preferences - Use
DeleteUser@permanentlyDelete()only after grace period
- Always use
-
Preserve balance preferences:
- Always store in
commentfield as JSON - Include
stored_attimestamp - Validate before permanent deletion
- Always store in
-
Testing:
- Use short grace period (1 day) in development
- Test both restoration and permanent deletion
- Verify email notifications
-
Cache management:
- Clear account balance cache when restoring
- Invalidate session data appropriately
- Refresh Elasticsearch indices if needed
Troubleshooting
Profile shows as deleted but can't be restored
Possible causes:
- Grace period already expired
- Profile already permanently deleted (email starts with
deleted_user_) - Database inconsistency
Solution:
# Check profile status in database
php artisan tinker
>>> $user = App\Models\User::withTrashed()->where('name', 'username')->first();
>>> $user->deleted_at
>>> $user->email
>>> Carbon\Carbon::parse($user->deleted_at)->addDays(config('timebank_cc.delete_profile.grace_period_days'))
Balance handling preferences lost
Possible causes:
commentfield was cleared manually- Profile updated after deletion
Solution:
- Check database for
commentfield content - If lost, you'll need to manually decide balance handling during restoration
Permanent deletion not running
Possible causes:
- Laravel scheduler not configured
- Command has errors
- No expired profiles
Solution:
# Check scheduler is running
php artisan schedule:list
# Run manually to see errors
php artisan profiles:permanently-delete-expired
# Check logs
tail -f storage/logs/laravel.log
tail -f storage/logs/permanent-deletions.log
Restored profile can't log in
Possible causes:
- Cache not cleared
- Session data corrupted
- Password was changed during soft deletion
Solution:
# Clear all caches
php artisan cache:clear
php artisan view:clear
php artisan config:clear
# Reset password if needed
php artisan tinker
>>> $user = App\Models\User::where('name', 'username')->first();
>>> $user->password = Hash::make('newpassword');
>>> $user->save();
Future Enhancements
Potential improvements to the grace period system:
-
Reminder emails:
- Send email 7 days before permanent deletion
- Send final warning 24 hours before deletion
-
Self-service restoration:
- Allow users to restore their own profiles via email link
- Magic link authentication for deleted accounts
-
Extended grace for specific cases:
- Longer grace period for profiles with high balance
- Longer grace for organization/bank profiles
-
Audit trail:
- Track who restored profiles and when
- Log reason for restoration
- Activity log entries for all grace period actions
-
Dashboard for admins:
- View all profiles in grace period
- Bulk restore operations
- Analytics on deletion patterns
WireChat Message Handling
All WireChat messages are eventually deleted to prevent orphaned data. When a profile is permanently deleted, their sent messages are released for cleanup.
Configuration
Location: config/timebank-*.php
'wirechat' => [
'disappearing_messages' => [
'allow_users_to_keep' => true, // Allow marking messages as "kept"
'duration' => 30, // Days before regular messages deleted
'kept_messages_duration' => 90, // Days before kept messages deleted
'cleanup_schedule' => 'everyFiveMinutes',
],
'profile_deletion' => [
'release_kept_messages' => true, // MUST be true to prevent orphans
],
],
Note: Disappearing messages are ALWAYS enabled. There is no 'enabled' flag - messages are always cleaned up to prevent orphaned data from deleted profiles.
Behavior
All messages are eventually deleted:
- Regular messages: Deleted after
durationdays (default: 30 days) - Kept messages: Deleted after
kept_messages_durationdays (default: 90 days) - Messages from deleted profiles: Released and then deleted after
durationdays
When profile is permanently deleted:
- Profile enters grace period (default: 30 days)
- Grace period expires → permanent deletion runs
- All kept messages sent by that profile:
kept_at = null(released) - Messages become regular messages, deleted after
durationdays - This prevents orphaned messages with invalid
sendable_id
Timeline Example
With default settings:
duration = 30dayskept_messages_duration = 90daysrelease_kept_messages = true
Scenario 1: Normal message lifecycle
Day 0: User sends message
Day 30: Message automatically deleted by DeleteExpiredWireChatMessagesJob
Scenario 2: Kept message lifecycle
Day 0: User sends message, recipient marks as "kept"
Day 90: Kept message automatically deleted by cleanup job
Scenario 3: Sender deletes profile
Day 0: User sends message, recipient marks as "kept"
Day 365: User deletes profile → grace period starts
Day 395: Grace period expires → permanent deletion
→ kept_at set to null (message "released")
→ Message now subject to normal 30-day duration
Day 425: Message deleted by cleanup job (30 days after release)
Why This Prevents Orphaned Messages
Without release_kept_messages:
- Deleted profile has
sendable_id = 123,sendable_type = 'App\Models\User' - Profile anonymized: name becomes "Removed user abc123ef"
- Kept messages still reference old
sendable_idpointing to anonymized profile - Messages orphaned forever with no way to trace original sender
- Database accumulates orphaned data indefinitely
With release_kept_messages = true:
- Kept messages released when profile permanently deleted
- Normal cleanup job deletes them after
durationdays - No orphaned data remains in database
- Clean data retention without privacy concerns
- Conversation history maintained for recipients until cleanup
Configuration Notes
release_kept_messagesshould ALWAYS betrue(default and recommended)- Setting it to
falsewill cause orphaned messages to accumulate - All durations specified in DAYS (converted to seconds internally)
- Cleanup job runs based on
cleanup_schedule(default: every 5 minutes) - User control is hardcoded to
false(users cannot change message duration) - Disappearing messages are hardcoded to enabled (always active)
Related Documentation
- Profile Auto-Delete:
references/PROFILE_AUTO_DELETE.md- Automatic deletion based on inactivity - Profile Inactive Config:
references/PROFILE_INACTIVE_CONFIG.md- Configuration for inactive profile detection - Email Testing:
references/EMAIL-TESTING-GUIDE.md- Testing email notifications
Changelog
Version 1.1 (2025-12-19)
- BREAKING CHANGE: Balance handling data moved from
commentfield to Redis cache - Added cache fallback: automatically destroys currency if cache is lost
- Comment field now human-readable:
- Auto-deletion: Shows translated message "Profile automatically deleted after X days of inactivity"
- Manual deletion: Empty (no comment)
- Added translation keys for auto-deletion comments in 5 languages (en, nl, fr, de, es)
- Enhanced logging: Warning logged when cache fallback is triggered
- Cache automatically expires after grace period + 7 days buffer
- Transaction table provides complete audit trail of balance handling
Version 1.0 (2025-12-19)
- Initial implementation of grace period system
- Two-phase deletion process (soft delete + permanent delete)
- Restore command with interactive and list modes
- Scheduled permanent deletion command
- Email notifications with grace period information
- Balance handling deferred until permanent deletion