Blue-Green Replace
Zero-downtime data replacement using staged generations and atomic swaps.
Blue-Green Replace
Replace all data in a collection with zero downtime. Upload a new dataset into a staging generation, verify it, then atomically swap it into production. The old data stays searchable until the swap completes.
How it works
- Upload —
POST /portal/collections/{name}/replacecreates a staging generation and uploads your file into it. - Monitor —
GET /portal/collections/{name}/replace-statustracks upload progress. All records must reachindexedstatus before swapping. - Swap —
POST /portal/collections/{name}/swapatomically promotes the staging generation in Postgres. - Cancel (optional) —
POST /portal/collections/{name}/cancel-replacearchives the staging generation.
Only one replace operation can be in progress per collection at a time.
POST /portal/collections/{name}/replace
Start a blue-green replace by uploading a file. Accepts the same file formats as regular file upload: CSV, JSON, NDJSON, XML, and PDF.
curl -X POST "https://api.qanatix.com/api/v1/portal/collections/manufacturing/replace?record_type=product" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-F "file=@new-catalog.csv"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
record_type | string | yes | Record type for the uploaded records |
source_name | string | no | Source label attached to each record |
record_tag | string | no | XML record tag name (required for XML files) |
Quota check
Before upload begins, the endpoint checks that the replacement will not exceed your plan's record limit. The check uses the post-swap count: records in other collections plus the new records being uploaded.
other_records + new_records <= record_limitIf the check fails, the upload is rejected with a 402 error before any data is written.
Response (200)
{
"upload_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"summary": {
"submitted": 2500,
"accepted": 2500,
"rejected": 0,
"dedup_skipped": 0
},
"errors": [],
"metadata": {
"processing_time_ms": 4500,
"file_hash": "sha256:abc...",
"reconciliation_passed": true,
"pipeline_backpressure": false
},
"staging": {
"generation": 2,
"status": "staging",
"collection": "manufacturing"
}
}The response combines the standard upload result fields with a staging object containing the new generation number and status.
Errors
| Status | Meaning |
|---|---|
402 | New dataset would exceed your plan's record limit |
409 | A replace operation is already in progress for this collection |
413 | Uploaded file exceeds the maximum size (50 MB) |
GET /portal/collections/{name}/replace-status
Check the progress of a pending replace operation.
curl https://api.qanatix.com/api/v1/portal/collections/manufacturing/replace-status \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"has_pending_replace": true,
"staging_generation": 2,
"staging_status": "staging",
"phase": "indexing",
"total": 2500,
"indexed": 2100,
"pending": 350,
"processing": 50,
"failed": 0,
"ready_to_swap": false
}Response fields
| Field | Type | Description |
|---|---|---|
has_pending_replace | boolean | Whether a staging generation exists |
staging_generation | integer | Generation number of the staging data |
staging_status | string | Always "staging" while in progress |
phase | string | Current phase of the replace operation (see below) |
total | integer | Total records in the staging generation |
indexed | integer | Records that are indexed and searchable |
pending | integer | Records waiting to be processed |
processing | integer | Records currently being processed |
failed | integer | Records that failed processing |
ready_to_swap | boolean | true when all records are indexed (no pending, processing, or failed) |
When has_pending_replace is false, all other fields are null.
Replace phases
The phase field tracks the lifecycle of a replace operation. Phases progress in this order:
| Phase | Description |
|---|---|
uploading | File is being uploaded and parsed |
queued | Records have been created and are waiting to be processed |
indexing | Records are actively being indexed |
completed_with_errors | Indexing finished but some records failed (check failed count) |
ready | All records are indexed and the staging generation is ready to swap |
processing | A swap or cancel operation is currently in progress |
Use the phase field to show detailed progress in your UI. The ready_to_swap boolean remains the authoritative signal for whether a swap can proceed.
POST /portal/collections/{name}/swap
Promote the staging generation to production. This performs two atomic operations:
- Postgres swap — staging records become the active generation; old records are hard-deleted (permanently removed, not archived).
Search traffic sees no interruption.
curl -X POST https://api.qanatix.com/api/v1/portal/collections/manufacturing/swap \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Preconditions
All staging records must be indexed. Check ready_to_swap in the replace-status response before calling this endpoint.
Response (200)
{
"collection": "manufacturing",
"old_generation": 1,
"new_generation": 2,
"old_record_count": 2000,
"new_record_count": 2500,
"deleted": 2000
}Response fields
| Field | Type | Description |
|---|---|---|
collection | string | Collection name |
old_generation | integer | Previous production generation (now deleted) |
new_generation | integer | New active generation |
old_record_count | integer | Number of records that were in the old generation |
new_record_count | integer | Number of records now in production |
deleted | integer | Number of old records permanently deleted |
Errors
| Status | Meaning |
|---|---|
409 | No staging generation exists, or staging records are not fully indexed |
POST /portal/collections/{name}/cancel-replace
Cancel a pending replace operation. Archives all staging records. The current production data is unaffected.
curl -X POST https://api.qanatix.com/api/v1/portal/collections/manufacturing/cancel-replace \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"collection": "manufacturing",
"cancelled_generation": 2,
"records_archived": 2500
}Response fields
| Field | Type | Description |
|---|---|---|
collection | string | Collection name |
cancelled_generation | integer | Generation number that was cancelled |
records_archived | integer | Number of staging records archived |
Errors
| Status | Meaning |
|---|---|
409 | No staging generation exists to cancel |
Complete example
A typical replace workflow for refreshing a product catalog:
# 1. Upload new dataset
curl -X POST "https://api.qanatix.com/api/v1/portal/collections/manufacturing/replace?record_type=product&source_name=catalog-q2" \
-H "Authorization: Bearer $JWT" \
-F "file=@catalog-q2-2026.csv"
# 2. Poll until ready
while true; do
STATUS=$(curl -s https://api.qanatix.com/api/v1/portal/collections/manufacturing/replace-status \
-H "Authorization: Bearer $JWT")
READY=$(echo $STATUS | jq -r '.ready_to_swap')
if [ "$READY" = "true" ]; then break; fi
echo "Waiting... $(echo $STATUS | jq -r '.indexed')/$(echo $STATUS | jq -r '.total') indexed"
sleep 5
done
# 3. Swap into production
curl -X POST https://api.qanatix.com/api/v1/portal/collections/manufacturing/swap \
-H "Authorization: Bearer $JWT"Error responses
All blue-green replace endpoints follow the standard error format:
{
"detail": "Replace already in progress for collection 'manufacturing'"
}| Status | Meaning |
|---|---|
401 | Missing or invalid JWT token |
402 | Record quota exceeded |
404 | Collection not found |
409 | Conflict (replace in progress, not ready, or no staging) |
413 | File too large |
422 | Validation error (missing record_type, invalid file format) |