Source:
ocean/docs/cleanup-automation.md| ✏️ Edit on GitHub
Cleanup Automation Guide
This guide explains the automated cleanup system for handling failed provisioning attempts and unverified accounts in the Ocean platform.
Overview
The cleanup automation system consists of:
- Database Functions - Identify and track cleanup candidates
- Edge Functions - Execute cleanup operations
- Scheduled Jobs - Automate regular cleanup runs
Components
1. Database Functions
get_failed_provisions(hours_threshold, include_incomplete)
- Identifies organizations with failed, stuck, or incomplete provisioning
- Parameters:
hours_threshold(default: 24) - How many hours old a provision must beinclude_incomplete(default: true) - Include stuck/incomplete provisions
get_cleanup_candidates(hours_threshold)
- Finds unverified user accounts older than the threshold
- Parameters:
hours_threshold(default: 72) - Hours since account creation
cleanup_unverified_accounts(hours_threshold, dry_run)
- Deletes unverified accounts and their organizations
- Parameters:
hours_threshold(default: 72) - Hours since account creationdry_run(default: false) - Simulate without deleting
record_cleanup_attempt(organization_id, cleanup_type, success, resources_cleaned, error_message)
- Records cleanup attempts for audit trail
- Creates entries in the
provisioning_eventstable
2. Edge Functions
/cleanup-failed-provisions
Handles cleanup of failed provisioning attempts.
Request:
{
"hoursThreshold": 24, // Age of provisions to cleanup
"includeIncomplete": true, // Include stuck provisions
"dryRun": false, // Test mode without deletion
"maxCleanups": 10 // Limit per run
}
Response:
{
"success": true,
"dryRun": false,
"totalFound": 15,
"processed": 10,
"successful": 9,
"failed": 1,
"stripeDeleted": 8,
"neonDeleted": 9,
"nextRunSuggested": true
}
/scheduled-cleanup
Unified cleanup function for both unverified accounts and failed provisions.
Query Parameters:
type:unverified,failed, orall(default:all)hours: Hours threshold (default: 24)dry_run: Test mode (default: false)
Example URLs:
/scheduled-cleanup?type=all&hours=24
/scheduled-cleanup?type=failed&hours=48&dry_run=true
/scheduled-cleanup?type=unverified&hours=72
3. Existing Functions
/cleanup-resources
- Deletes external resources (Stripe customers, Neon databases)
- Called by other cleanup functions
- Handles individual resource deletion
Setting Up Scheduled Cleanup
Option 1: Supabase Cron Jobs (Recommended)
Add to your Supabase project's cron jobs:
-- Daily cleanup of failed provisions (24+ hours old)
SELECT
cron.schedule(
'cleanup-failed-provisions',
'0 2 * * *', -- Run at 2 AM UTC daily
$$
SELECT
net.http_post(
url := current_setting('app.settings.supabase_url') || '/functions/v1/scheduled-cleanup?type=failed&hours=24',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.settings.supabase_service_role_key')
)
);
$$
);
-- Weekly cleanup of unverified accounts (72+ hours old)
SELECT
cron.schedule(
'cleanup-unverified-accounts',
'0 3 * * 0', -- Run at 3 AM UTC on Sundays
$$
SELECT
net.http_post(
url := current_setting('app.settings.supabase_url') || '/functions/v1/scheduled-cleanup?type=unverified&hours=72',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.settings.supabase_service_role_key')
)
);
$$
);
Option 2: External Cron Service
Use services like GitHub Actions, Vercel Cron, or cron-job.org:
# Daily cleanup
curl -X POST https://your-project.supabase.co/functions/v1/scheduled-cleanup?type=all \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY"
Option 3: GitHub Actions (Example)
name: Cleanup Failed Provisions
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
workflow_dispatch: # Manual trigger
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Run Cleanup
run: |
curl -X POST ${{ secrets.SUPABASE_URL }}/functions/v1/scheduled-cleanup?type=all \
-H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}" \
-H "Content-Type: application/json"
Testing Cleanup
1. Dry Run Mode
Always test with dry run first:
# Test failed provisions cleanup
curl -X POST https://your-project.supabase.co/functions/v1/cleanup-failed-provisions \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"hoursThreshold": 1,
"dryRun": true
}'
2. Monitor Cleanup Status
Query the monitoring view:
SELECT * FROM cleanup_monitoring;
3. Check Provisioning Events
Review cleanup history:
SELECT *
FROM provisioning_events
WHERE event_type LIKE 'cleanup_%'
ORDER BY created_at DESC
LIMIT 20;
Best Practices
- Start Conservative: Begin with high hour thresholds (48-72 hours)
- Use Dry Run: Always test with
dryRun: truefirst - Monitor Regularly: Check cleanup logs and error rates
- Gradual Rollout: Start with small
maxCleanupsvalues - Alert on Failures: Set up alerts for cleanup errors
Troubleshooting
Common Issues
-
"Unauthorized" errors
- Ensure you're using the service role key
- Check the authorization header format
-
Resources not deleted
- Check if Stripe/Neon APIs are available
- Verify API keys are correctly configured
- Review circuit breaker status in logs
-
Partial cleanups
- Normal for resources with dependencies
- Review error messages in response
- Check provisioning_events for details
Manual Cleanup
For specific organizations:
curl -X POST https://your-project.supabase.co/functions/v1/cleanup-resources \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "delete_all",
"organizationId": "org-uuid-here"
}'
Security Considerations
- Authorization: Only service role keys can trigger cleanup
- Audit Trail: All cleanup attempts are logged
- Safe Defaults: Conservative thresholds prevent accidental deletion
- Rollback Protection: Failed provisions already use atomic rollback
Monitoring
Use the cleanup monitoring view to track:
- Failed provisions count
- Stuck provisions count
- Blocked provisions count
- Oldest issue date
- Most recent event
-- Real-time cleanup metrics
SELECT * FROM cleanup_monitoring;
-- Cleanup success rate (last 7 days)
SELECT
COUNT(*) FILTER (WHERE status = 'completed') AS successful,
COUNT(*) FILTER (WHERE status = 'failed') AS failed,
COUNT(*) AS total
FROM provisioning_events
WHERE event_type LIKE 'cleanup_%'
AND created_at > NOW() - INTERVAL '7 days';