================================================================================ INDELIS — Deployment Runbook: Migration 0007 (Document Vault) Date: 2026-06-24 Feature: INDL-05 Document Vault ================================================================================ PRE-DEPLOY CHECKLIST ──────────────────── 1. Confirm environment variables are set in the target .env: S3_DOCUMENTS_BUCKET=indelis-dev-uat S3_DOCUMENTS_REGION=us-east-1 CLOUDFRONT_DOCUMENTS_URL=https://d16xufzn5inupa.cloudfront.net AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= 2. Confirm the IAM user `indelis-dev-uat_s3_user` has the following S3 permissions on the `indelis-dev-uat` bucket: - s3:PutObject - s3:GetObject - s3:DeleteObject - s3:ListBucket - s3:PutObjectAcl (only if ACLs are enabled on the bucket) 3. Confirm the S3 bucket `indelis-dev-uat` exists in us-east-1 and that public access block settings match your intended access pattern. For presigned URL uploads, the bucket does NOT need to be public. 4. Confirm the CORS configuration below has been applied to the bucket (see S3 CORS section at the bottom of this file). 5. Confirm CloudFront distribution `d16xufzn5inupa.cloudfront.net` has the `indelis-dev-uat` bucket as its origin. 6. Confirm alembic version is at 0006 before running this migration: docker-compose exec api alembic current Expected output should show head at 0006. If it shows 0007 already, the migration has already been applied — skip the upgrade step. 7. Take a database backup before running in staging/production: docker-compose exec postgres pg_dump -U postgres indelis > backup_pre_0007.sql MIGRATION: APPLY ──────────────── Run migration 0007 (documents table + RLS policy): docker-compose exec api alembic upgrade head Verify the table was created: docker-compose exec postgres psql -U postgres indelis \ -c "\d documents" Expected columns: id, tenant_id, entity_type, entity_id, s3_key, filename, file_size_bytes, mime_type, category, version, uploaded_by, upload_confirmed, created_at, updated_at, deleted_at MIGRATION: ROLLBACK ─────────────────── If the deploy must be rolled back, run: docker-compose exec api alembic downgrade -1 This will: - DROP POLICY tenant_isolation ON documents - DISABLE ROW LEVEL SECURITY on documents - Drop indexes: ix_documents_pending, ix_documents_tenant, ix_documents_entity - DROP TABLE documents IMPORTANT: Any uploaded document metadata stored in the documents table will be permanently lost on downgrade. S3 objects are NOT deleted by the migration — clean up manually if needed. SMOKE TEST ────────── After migration and service restart, verify the documents endpoint is reachable and returns a valid response (empty list expected on a fresh tenant): curl -s \ -H "Authorization: Bearer " \ -H "X-Tenant-Slug: " \ http://localhost:8000/api/v1/records//documents \ | python3 -m json.tool Expected response shape: { "success": true, "message": "...", "data": { "items": [], "total": 0, "page": 1, "page_size": 20, "pages": 0 } } To test presigned POST generation (document upload initiation): curl -s -X POST \ -H "Authorization: Bearer " \ -H "X-Tenant-Slug: " \ -H "Content-Type: application/json" \ -d '{"filename": "test.pdf", "mime_type": "application/pdf", "entity_type": "record", "entity_id": ""}' \ http://localhost:8000/api/v1/documents/upload-url \ | python3 -m json.tool A successful response will contain a `url` and `fields` dict for direct browser-to-S3 upload via multipart/form-data POST. S3 CORS CONFIGURATION ───────────────────── The following CORS JSON must be applied to the `indelis-dev-uat` S3 bucket to allow presigned POST uploads directly from the frontend. Apply via AWS Console: S3 → indelis-dev-uat → Permissions → Cross-origin resource sharing (CORS) → Edit Or via AWS CLI (do not run this automatically — apply manually after review): aws s3api put-bucket-cors \ --bucket indelis-dev-uat \ --cors-configuration file://cors.json CORS JSON (save as cors.json before applying): [ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "PUT", "POST", "DELETE", "HEAD" ], "AllowedOrigins": [ "http://localhost:3000", "http://localhost:3001", "https://*.indelis.com" ], "ExposeHeaders": [ "ETag", "x-amz-request-id", "x-amz-id-2" ], "MaxAgeSeconds": 3600 } ] Notes: - AllowedHeaders: ["*"] is required for presigned POST — the browser sends the policy, x-amz-signature, and x-amz-credential headers which must be allowed. - POST is required for presigned POST (generate_presigned_post). PUT is required if presigned PUT (generate_presigned_url) is also used. - Localhost origins cover local development. The *.indelis.com wildcard covers all tenant subdomains in staging and production. - Do NOT add "https://d16xufzn5inupa.cloudfront.net" to AllowedOrigins — CloudFront is a read origin for download, not an upload origin. Uploads go directly to S3; reads go through CloudFront. - For production, consider tightening AllowedHeaders to the specific headers boto3 generates rather than using the wildcard. SERVICE RESTART ─────────────── After applying env vars and running the migration, restart the api and worker: docker-compose restart api worker Confirm the api is healthy: docker-compose exec api curl -f http://localhost:8000/health/ready ================================================================================ END OF RUNBOOK ================================================================================