Skip to main content

FEATURE: Customer Backup System

Overview

Enable CODITECT customers to backup and restore their context database and Claude config files through the CODITECT Cloud API, without requiring direct GCS access or gcloud authentication.

Business Value

  • Data Protection: Customers can recover from data loss
  • Multi-Device Sync: Restore context on new machines
  • Enterprise Compliance: Audit trails, retention policies
  • Upsell Opportunity: Tiered backup retention by plan

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ CUSTOMER MACHINE │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ context.db │ │ backup-context-db.sh --cloud │ │
│ │ Claude config│───▶│ │ │
│ └──────────────┘ │ 1. Compress files │ │
│ │ 2. Read license key │ │
│ │ 3. Upload via API │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘
│ HTTPS + API Key

┌─────────────────────────────────────────────────────────────────┐
│ api.coditect.ai │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ BackupViewSet │ │
│ │ POST /api/v1/backup/upload - Upload backup │ │
│ │ GET /api/v1/backup/list - List backups │ │
│ │ GET /api/v1/backup/{id} - Get backup details │ │
│ │ GET /api/v1/backup/{id}/download - Download backup │ │
│ │ DELETE /api/v1/backup/{id} - Delete backup │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┴───────────────────────────────┐ │
│ │ BackupService │ │
│ │ - Tenant isolation (auto via django-multitenant) │ │
│ │ - Retention policy enforcement │ │
│ │ - Storage quota management │ │
│ │ - Encryption at rest │ │
│ └────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ GCS: gs://coditect-customer-backups/ │
│ │
│ /{tenant_id}/{user_id}/{timestamp}/ │
│ ├── context.db.gz.enc │
│ ├── unified_messages.jsonl.gz.enc │
│ ├── unified_hashes.json.gz.enc │
│ ├── unified_stats.json.gz.enc │
│ ├── claude-config/ │
│ │ ├── settings.json.gz.enc │
│ │ ├── settings.local.json.gz.enc │
│ │ └── statusline-config.json.gz.enc │
│ └── manifest.json │
└─────────────────────────────────────────────────────────────────┘

Pricing Tiers

PlanRetentionMax BackupsMax SizeAuto-Backup
Free7 days31 GBNo
Pro30 days1010 GBDaily
Team90 days3050 GBDaily
Enterprise1 yearUnlimitedUnlimitedConfigurable

Task Breakdown

Track A: Backend API

A.11.1 Create Backup Models

Status: [ ] Pending Effort: 2 hours Files:

  • backend/coditect_license/backup/models.py
class Backup(TenantModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
size_bytes = models.BigIntegerField()
storage_path = models.CharField(max_length=500)
manifest = models.JSONField() # File list, checksums
status = models.CharField(choices=BACKUP_STATUS, default='pending')
retention_until = models.DateTimeField()

class Meta:
indexes = [
models.Index(fields=['user', '-created_at']),
]

A.11.2 Create Backup Serializers

Status: [ ] Pending Effort: 1 hour Files:

  • backend/coditect_license/backup/serializers.py
class BackupSerializer(serializers.ModelSerializer):
download_url = serializers.SerializerMethodField()

class Meta:
model = Backup
fields = ['id', 'created_at', 'size_bytes', 'manifest',
'status', 'retention_until', 'download_url']

A.11.3 Create BackupService

Status: [ ] Pending Effort: 4 hours Files:

  • backend/coditect_license/backup/services.py
class BackupService:
def __init__(self, tenant, user):
self.tenant = tenant
self.user = user
self.storage = GCSBackupStorage()

def create_backup(self, files: List[UploadedFile]) -> Backup:
"""Upload files to GCS, create Backup record."""
pass

def list_backups(self) -> QuerySet[Backup]:
"""List user's backups (tenant-isolated)."""
pass

def get_download_url(self, backup_id: UUID) -> str:
"""Generate signed URL for download."""
pass

def delete_backup(self, backup_id: UUID) -> None:
"""Delete backup from GCS and database."""
pass

def enforce_retention(self) -> int:
"""Delete expired backups, return count."""
pass

def check_quota(self, size_bytes: int) -> bool:
"""Check if user has quota for new backup."""
pass

A.11.4 Create BackupViewSet

Status: [ ] Pending Effort: 3 hours Files:

  • backend/coditect_license/backup/views.py
class BackupViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, HasValidLicense]
serializer_class = BackupSerializer

@action(detail=False, methods=['post'])
def upload(self, request):
"""Upload new backup (multipart form)."""
pass

@action(detail=True, methods=['get'])
def download(self, request, pk=None):
"""Get signed download URL."""
pass

A.11.5 Create GCS Storage Backend

Status: [ ] Pending Effort: 3 hours Files:

  • backend/coditect_license/backup/storage.py
class GCSBackupStorage:
BUCKET = 'coditect-customer-backups'

def upload(self, tenant_id, user_id, files) -> str:
"""Upload files, return storage path."""
pass

def download_url(self, path: str, expiry: int = 3600) -> str:
"""Generate signed download URL."""
pass

def delete(self, path: str) -> None:
"""Delete backup from GCS."""
pass

A.11.6 Add URL Routes

Status: [ ] Pending Effort: 30 min Files:

  • backend/coditect_license/backup/urls.py
  • backend/coditect_license/urls.py
router.register(r'backup', BackupViewSet, basename='backup')

A.11.7 Create Retention Celery Task

Status: [ ] Pending Effort: 2 hours Files:

  • backend/coditect_license/backup/tasks.py
@shared_task
def enforce_backup_retention():
"""Run daily to delete expired backups."""
for tenant in Tenant.objects.all():
set_current_tenant(tenant)
service = BackupService(tenant, None)
deleted = service.enforce_retention()
logger.info(f"Tenant {tenant.id}: deleted {deleted} expired backups")

Track C: Infrastructure

C.6.1 Create Customer Backup GCS Bucket

Status: [ ] Pending Effort: 1 hour Commands:

gsutil mb -l us-central1 -c STANDARD gs://coditect-customer-backups
gsutil iam ch serviceAccount:backend@coditect-citus-prod.iam.gserviceaccount.com:objectAdmin gs://coditect-customer-backups

C.6.2 Configure Bucket Lifecycle

Status: [ ] Pending Effort: 30 min Files:

  • infra/gcs-lifecycle-customer-backups.json
{
"lifecycle": {
"rule": [
{
"action": {"type": "Delete"},
"condition": {"age": 365, "matchesPrefix": ["free/"]}
}
]
}
}

C.6.3 Add Backend Service Account Permissions

Status: [ ] Pending Effort: 30 min

C.6.4 Update Terraform/Infrastructure as Code

Status: [ ] Pending Effort: 2 hours Files:

  • infra/terraform/gcs.tf

Track A: Client Script Updates

A.11.8 Add --cloud Flag to backup-context-db.sh

Status: [ ] Pending Effort: 3 hours Files:

  • scripts/backup-context-db.sh
# New cloud backup mode
cloud_backup() {
local license_file="$HOME/.coditect/licensing/license.json"

# Check license
if [[ ! -f "$license_file" ]]; then
log_error "No license found. Run: /license-activate <KEY>"
exit 1
fi

# Extract API key
local api_key=$(jq -r '.license_key' "$license_file")
local api_url="https://api.coditect.ai/api/v1/backup/upload"

# Prepare files (compress)
prepare_backup_files

# Upload via API
curl -X POST "$api_url" \
-H "Authorization: Bearer $api_key" \
-F "context_db=@$TEMP_DIR/context.db.gz" \
-F "unified_messages=@$TEMP_DIR/unified_messages.jsonl.gz" \
-F "claude_config=@$TEMP_DIR/claude-config.tar.gz"
}

A.11.9 Add --cloud-list and --cloud-restore

Status: [ ] Pending Effort: 2 hours Files:

  • scripts/backup-context-db.sh
cloud_list() {
curl -s "https://api.coditect.ai/api/v1/backup/" \
-H "Authorization: Bearer $api_key" | jq '.results'
}

cloud_restore() {
local backup_id="$1"
local download_url=$(curl -s "https://api.coditect.ai/api/v1/backup/$backup_id/download" \
-H "Authorization: Bearer $api_key" | jq -r '.url')

# Download and extract
curl -s "$download_url" | tar -xz -C "$CONTEXT_DIR"
}

A.11.10 Auto-Detect Mode (Local vs Cloud)

Status: [ ] Pending Effort: 1 hour

# Auto-detect: use cloud if licensed, local if gcloud available
detect_backup_mode() {
if [[ -f "$LICENSE_FILE" ]]; then
echo "cloud"
elif command -v gcloud &> /dev/null; then
echo "local"
else
log_error "No backup method available"
exit 1
fi
}

Track E: Testing

E.6.1 Unit Tests for BackupService

Status: [ ] Pending Effort: 3 hours Files:

  • backend/coditect_license/backup/tests/test_services.py

E.6.2 API Integration Tests

Status: [ ] Pending Effort: 2 hours Files:

  • backend/coditect_license/backup/tests/test_views.py

E.6.3 Script Integration Tests

Status: [ ] Pending Effort: 2 hours Files:

  • tests/integration/test_backup_cloud.py

E.6.4 Tenant Isolation Tests

Status: [ ] Pending Effort: 2 hours


Track F: Documentation

F.5.1 Customer Backup Guide

Status: [ ] Pending Effort: 2 hours Files:

  • docs/guides/BACKUP-GUIDE.md

F.5.2 API Documentation

Status: [ ] Pending Effort: 1 hour Files:

  • docs/api/backup-api.md

F.5.3 Update /backup Command

Status: [ ] Pending Effort: 30 min Files:

  • commands/backup.md

Dependencies

Milestones

MilestoneTasksTarget
M1: Backend APIA.11.1-A.11.7Week 1
M2: InfrastructureC.6.1-C.6.4Week 1
M3: Client IntegrationA.11.8-A.11.10Week 2
M4: TestingE.6.1-E.6.4Week 2
M5: DocumentationF.5.1-F.5.3Week 2
M6: LaunchDeploy to prodWeek 3

Security Considerations

  1. Encryption at Rest: All backups encrypted with tenant-specific keys
  2. Signed URLs: Download URLs expire after 1 hour
  3. Tenant Isolation: django-multitenant ensures data separation
  4. Rate Limiting: Max 10 backups per day per user
  5. Size Limits: Enforced per pricing tier

Rollback Plan

If issues arise:

  1. Disable /backup --cloud flag
  2. Customers fall back to local GCS backup (if configured)
  3. Existing backups remain accessible via API

Created: 2026-01-18 Author: Claude Opus 4.5 + Hal Casteel ADR: ADR-184 (customer-backup-architecture)