Delete & Recovery
Branch and project deletion procedures, data cleanup, audit trails, and recovery expectations in AxiomDB
Delete & Recovery
AxiomDB supports hard deletion for both branches and projects. This guide covers exactly what gets deleted, what is preserved, confirmation requirements, and recovery expectations.
Branch Deletion
What happens when a branch is hard-deleted: database, roles, credentials, and metadata
Project Deletion
Owner-only project deletion with typed confirmation
Data Cleanup
Detailed breakdown of every resource removed during deletion
Audit Trail
What audit records are preserved after deletion
Recovery
What can and cannot be recovered after deletion
Prevention
Safeguards to prevent accidental deletion
Branch Deletion
A branch hard delete is an irreversible operation that removes all resources associated with the branch from the system.
Deletion Sequence
┌──────────────────────────────────────────────────────────────┐
│ Branch Hard Delete Sequence │
├──────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Validation │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Verify caller has delete permission │ │
│ │ • Check branch is not in provisioning state │ │
│ │ • Confirm no active restore in progress │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: Database Drop │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Terminate all active connections to branch DB │ │
│ │ • DROP DATABASE <branch_db_name> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: Role Cleanup │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • REVOKE all privileges │ │
│ │ • DROP ROLE <branch_owner_role> │ │
│ │ • DROP ROLE <branch_readonly_role> (if exists) │ │
│ │ • DROP ROLE <branch_readwrite_role> (if exists) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 4: PgBouncer Cleanup │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Remove database entry from pgbouncer.ini │ │
│ │ • Remove user entries from pgbouncer userlist │ │
│ │ • Reload PgBouncer configuration │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 5: Credential Revocation │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Revoke all connection credentials │ │
│ │ • Invalidate API keys associated with branch │ │
│ │ • Remove credentials from secrets manager │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 6: Network Grant Cleanup │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Remove pg_hba.conf entries for branch │ │
│ │ • Remove CIDR grants from network policy │ │
│ │ • Reload PostgreSQL for pg_hba changes │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 7: Metadata Removal │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Delete branch record from AxiomDB metadata DB │ │
│ │ • Remove from project's branch list │ │
│ │ • Update project storage usage │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 8: Audit Log │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Record deletion event in audit log │ │
│ │ • PRESERVED indefinitely for compliance │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘API Call
# Delete a branch via the Gateway API
curl -X DELETE http://127.0.0.1:4060/api/branches/<branch_id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json"Response:
{
"status": "deleted",
"branch_id": "br_abc123def456",
"branch_name": "feature-auth",
"deleted_at": "2025-01-15T14:30:00Z",
"resources_removed": {
"database": "branch_feature_auth",
"roles_dropped": [
"branch_feature_auth_owner",
"branch_feature_auth_ro",
"branch_feature_auth_rw"
],
"pgbouncer_users_removed": 3,
"credentials_revoked": 2,
"network_grants_removed": 1
}
}Using square-dbctl
# Delete a branch
square-dbctl delete --branch feature-auth --confirm
# Force delete (skip confirmation prompt)
square-dbctl delete --branch feature-auth --forceProject Deletion
Project deletion removes the project and all branches within it. This is a cascading destructive operation.
Requirements
- Only the project owner can delete a project
- Caller must type the project name exactly to confirm
- All branches must be individually deletable (not stuck in provisioning)
Confirmation Flow
┌──────────────────────────────────────────────────────┐
│ Project Deletion Confirmation │
├──────────────────────────────────────────────────────┤
│ │
│ API Request: │
│ DELETE /api/projects/<project_id> │
│ │
│ Required Headers: │
│ Authorization: Bearer <owner_token> │
│ Content-Type: application/json │
│ │
│ Required Body: │
│ { │
│ "confirm": "<exact_project_name>", │
│ "acknowledge_data_loss": true │
│ } │
│ │
│ Validation: │
│ ✓ Caller is project owner │
│ ✓ confirm matches project name exactly │
│ ✓ acknowledge_data_loss is true │
│ ✓ All branches are in deletable state │
│ │
│ If any check fails → 403 Forbidden │
│ │
└──────────────────────────────────────────────────────┘API Call
# Delete a project (requires owner auth + typed confirmation)
curl -X DELETE http://127.0.0.1:4060/api/projects/<project_id> \
-H "Authorization: Bearer <owner_token>" \
-H "Content-Type: application/json" \
-d '{
"confirm": "my-project-name",
"acknowledge_data_loss": true
}'Response:
{
"status": "deleted",
"project_id": "proj_xyz789",
"project_name": "my-project-name",
"deleted_at": "2025-01-15T14:30:00Z",
"branches_deleted": [
{
"branch_id": "br_abc123",
"branch_name": "main"
},
{
"branch_id": "br_def456",
"branch_name": "staging"
},
{
"branch_id": "br_ghi789",
"branch_name": "feature-auth"
}
],
"total_resources_removed": {
"databases": 3,
"roles": 9,
"credentials": 6,
"network_grants": 3
}
}Error Responses
// Not the project owner
{
"error": "forbidden",
"message": "Only the project owner can delete a project",
"status": 403
}
// Confirmation doesn't match
{
"error": "confirmation_mismatch",
"message": "The 'confirm' field must match the project name exactly: 'my-project-name'",
"status": 400
}
// acknowledge_data_loss not set
{
"error": "missing_acknowledgement",
"message": "You must set acknowledge_data_loss to true to delete a project",
"status": 400
}
// Branch stuck in provisioning
{
"error": "branch_not_deletable",
"message": "Branch 'feature-auth' is currently provisioning and cannot be deleted",
"status": 409
}Data Cleanup
Complete Resource Inventory
When a branch is hard-deleted, here is every resource type that is removed:
| Resource Type | Location | Cleanup Method | Reversible? |
|---|---|---|---|
| PostgreSQL Database | postgres:5432 | DROP DATABASE | No |
| Owner Role | postgres:5432 | DROP ROLE | No |
| Read-Only Role | postgres:5432 | DROP ROLE | No |
| Read-Write Role | postgres:5432 | DROP ROLE | No |
| PgBouncer Database Entry | pgbouncer.ini | Config removal | No |
| PgBouncer User Entries | pgbouncer userlist.txt | File removal | No |
| Connection Credentials | Secrets Manager | API call | No |
| API Keys | AxiomDB Metadata DB | DB delete | No |
| pg_hba.conf Entries | pg_hba.conf | Line removal | No |
| Network CIDR Grants | AxiomDB Metadata DB | DB delete | No |
| Branch Metadata | AxiomDB Metadata DB | DB delete | No |
| Migration History | branch._prisma_migrations | Dropped with DB | No |
| WAL Archives | pgBackRest repository | Auto-expired | Eventually |
| Backups | pgBackRest repository | Auto-expired | Eventually |
What is Preserved
| Resource Type | Location | Retention Period | Purpose |
|---|---|---|---|
| Audit Log Entry | AxiomDB Audit DB | Indefinite | Compliance, forensics |
| Deletion Event | AxiomDB Audit DB | Indefinite | Record of what was removed |
| pgBackRest Backups | pgBackRest repo | Per retention policy | Recovery window |
Audit Preservation
Audit logs are never deleted, even when a branch or project is removed. They record: who performed the deletion, when it occurred, what resources were removed, and the IP address of the caller. This is essential for compliance and incident investigation.
SQL Verification After Deletion
-- Verify database is dropped
SELECT datname FROM pg_database WHERE datname = 'branch_feature_auth';
-- Should return 0 rows
-- Verify roles are dropped
SELECT rolname FROM pg_roles WHERE rolname LIKE 'branch_feature_auth%';
-- Should return 0 rows
-- Verify no orphaned connections
SELECT datname, usename FROM pg_stat_activity
WHERE datname = 'branch_feature_auth';
-- Should return 0 rows# Verify PgBouncer cleanup
psql -h 127.0.0.1 -p 6432 -U pgbouncer -d pgbouncer -c "SHOW DATABASES;" | grep feature_auth
# Should return no results
# Verify network grants removed
psql -h 127.0.0.1 -p 5432 -U axiomdb -c "
SELECT * FROM pg_hba_file_rules WHERE database LIKE '%feature_auth%';
"
-- Should return 0 rowsAudit Trail
Audit Event Schema
{
"event_id": "evt_20250115143000001",
"event_type": "branch.hard_delete",
"timestamp": "2025-01-15T14:30:00.000Z",
"actor": {
"user_id": "usr_abc123",
"email": "admin@example.com",
"ip_address": "203.0.113.42",
"user_agent": "curl/8.1.2"
},
"target": {
"branch_id": "br_abc123def456",
"branch_name": "feature-auth",
"project_id": "proj_xyz789",
"project_name": "my-project"
},
"resources_removed": {
"database": "branch_feature_auth",
"roles": [
"branch_feature_auth_owner",
"branch_feature_auth_ro",
"branch_feature_auth_rw"
],
"pgbouncer_users": 3,
"credentials_revoked": 2,
"network_grants": 1
},
"metadata": {
"reason": "user_requested",
"storage_freed_bytes": 1073741824,
"duration_ms": 1250
}
}Querying Audit Logs
# List deletion events for a project
curl -s "http://127.0.0.1:4060/api/projects/<project_id>/audit?event_type=branch.hard_delete" \
-H "Authorization: Bearer <token>" | jq .
# List all deletion events (admin only)
curl -s "http://127.0.0.1:4060/api/audit?event_type=branch.hard_delete,project.hard_delete&limit=50" \
-H "Authorization: Bearer <admin_token>" | jq .-- Direct audit log query (admin access)
SELECT
event_id,
event_type,
timestamp,
actor_email,
target_branch_name,
target_project_name,
reason
FROM audit_log
WHERE event_type IN ('branch.hard_delete', 'project.hard_delete')
ORDER BY timestamp DESC
LIMIT 50;Recovery Expectations
What CAN Be Recovered
| Scenario | Recovery Method | Timeframe | Notes |
|---|---|---|---|
| Branch deleted < backup retention | Restore from pgBackRest backup into new branch | Within retention window (default 7 days) | Requires pgBackRest backup to exist |
| Project deleted < backup retention | Restore each branch from backup | Within retention window | Each branch restored individually |
| Need data from deleted branch | pgBackRest point-in-time recovery | Within retention window | May need WAL replay |
What CANNOT Be Recovered
| Resource | Why | Workaround |
|---|---|---|
| Original branch ID | IDs are not reused | New branch gets new ID |
| Connection credentials | Cryptographically invalidated | Generate new credentials |
| API keys | Revoked from secrets manager | Generate new keys |
| PgBouncer entries | Removed from config | Recreated with new branch |
| Network grants | Removed from pg_hba.conf | Reconfigure on new branch |
| Branch metadata | Deleted from AxiomDB DB | Recreated with new branch |
| In-flight migrations | Lost with database | Re-run migrations after restore |
Recovery Procedure
# Step 1: Find the backup to restore from
pgbackrest --stanza=axiomdb info
# Look for backups that include the deleted branch's data
# Step 2: Create a new branch from the backup
curl -X POST http://127.0.0.1:4060/api/branches \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "restored-feature-auth",
"project_id": "proj_xyz789",
"restore_from": {
"backup_label": "20250115-020001F",
"database_name": "branch_feature_auth"
}
}'
# Step 3: Verify the restored branch
psql -h 127.0.0.1 -p 5432 -U axiomdb_restored_feature_auth \
-d restored_feature_auth -c "
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' ORDER BY table_name;
"
# Step 4: Reconfigure network grants
curl -X POST http://127.0.0.1:4060/api/branches/<new_branch_id>/network-grants \
-H "Content-Type: application/json" \
-d '{"cidr": "10.0.0.0/8", "description": "internal network"}'
# Step 5: Generate new credentials
curl -X POST http://127.0.0.1:4060/api/branches/<new_branch_id>/credentials \
-H "Authorization: Bearer <token>"Recovery Window
After the pgBackRest retention period expires (default: 7 days for full backups), deleted branch data is permanently unrecoverable. If you anticipate needing recovery, extend retention before the window closes or create a manual snapshot.
Deletion Prevention
Safeguards
AxiomDB implements several safeguards against accidental deletion:
- Confirmation Required: All deletions require explicit API confirmation
- Project Name Typing: Project deletion requires typing the exact project name
- Owner-Only: Only project owners can delete projects
- State Checks: Branches in provisioning cannot be deleted
- Rate Limiting: Deletion API calls are rate-limited to prevent rapid cascading deletes
- Audit Logging: All deletion attempts (successful or failed) are logged
Recommended Practices
□ Enable deletion protection on production branches
□ Set up alerts for deletion API calls
□ Require manual review for deletions in CI/CD pipelines
□ Use branch naming conventions that distinguish prod from dev
□ Regular backup verification to ensure recovery is possible
□ Document branch ownership and purpose
□ Use separate projects for production and developmentDeletion Protection API
# Enable deletion protection on a branch
curl -X PATCH http://127.0.0.1:4060/api/branches/<branch_id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"deletion_protection": true}'
# Attempting to delete a protected branch returns:
# {
# "error": "deletion_protected",
# "message": "This branch has deletion protection enabled. Disable it first.",
# "status": 403
# }
# Disable deletion protection (requires confirmation)
curl -X PATCH http://127.0.0.1:4060/api/branches/<branch_id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"deletion_protection": false,
"confirm_disable_protection": true
}'Alerting on Deletions
# Add to your alerting script
# Alert on any deletion API call
DELETION_COUNT=$(psql -h 127.0.0.1 -p 5432 -U axiomdb -t -c "
SELECT count(*) FROM audit_log
WHERE event_type IN ('branch.hard_delete', 'project.hard_delete')
AND timestamp > now() - interval '5 minutes';
" | tr -d ' ')
if [ "$DELETION_COUNT" -gt 0 ]; then
curl -s -X POST "$SLACK_WEBHOOK" -d "{
\"text\": \"⚠️ $DELETION_COUNT deletion(s) in the last 5 minutes. Check audit log.\"
}"
fiAppendix: Deletion Error Codes
| HTTP Code | Error | Description |
|---|---|---|
| 400 | confirmation_mismatch | Typed confirmation doesn't match resource name |
| 400 | missing_acknowledgement | acknowledge_data_loss not set to true |
| 403 | forbidden | Caller lacks delete permission |
| 403 | not_owner | Caller is not the project owner |
| 403 | deletion_protected | Resource has deletion protection enabled |
| 404 | not_found | Branch or project doesn't exist |
| 409 | branch_not_deletable | Branch is in provisioning or restore state |
| 429 | rate_limited | Too many deletion requests |
| 500 | deletion_failed | Internal error during deletion (partial cleanup may have occurred) |