Documentation Index
Fetch the complete documentation index at: https://mintlify.com/opengisch/qfieldcloud/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Deltas are the fundamental unit of change in QFieldCloud’s synchronization system. Each delta represents a single edit operation (create, update, or delete) performed in QField mobile app. The delta system enables offline editing with robust conflict detection and resolution.
Delta Model
The Delta model tracks individual edit operations:
class Delta(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
deltafile_id = models.UUIDField(db_index=True)
client_id = models.UUIDField(db_index=True)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="deltas")
content = JSONField()
last_status = models.CharField(choices=Status.choices, default=Status.PENDING)
last_feedback = JSONField(null=True)
last_modified_pk = models.TextField(null=True)
last_apply_attempt_at = models.DateTimeField(null=True)
last_apply_attempt_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="uploaded_deltas")
old_geom = models.GeometryField(null=True, srid=4326, dim=4)
new_geom = models.GeometryField(null=True, srid=4326, dim=4)
See core/models.py:2103.
Delta Methods
Three types of operations:
class Method(str, Enum):
Create = "create"
Delete = "delete"
Patch = "patch"
Create
Adds a new feature to a layer:
{
"method": "create",
"localLayerId": "points_layer",
"new": {
"geometry": {
"type": "Point",
"coordinates": [7.5, 46.5]
},
"attributes": {
"name": "New Survey Point",
"date": "2024-01-15"
}
}
}
Patch
Modifies an existing feature’s attributes or geometry:
{
"method": "patch",
"localLayerId": "points_layer",
"localPk": "123",
"old": {
"attributes": {
"status": "pending"
}
},
"new": {
"attributes": {
"status": "completed"
}
}
}
Delete
Removes a feature from a layer:
{
"method": "delete",
"localLayerId": "points_layer",
"localPk": "123",
"old": {
"geometry": {...},
"attributes": {...}
}
}
Delta Status
Deltas progress through various status states:
class Status(models.TextChoices):
PENDING = "pending", "Pending"
STARTED = "started", "Started"
APPLIED = "applied", "Applied"
CONFLICT = "conflict", "Conflict"
NOT_APPLIED = "not_applied", "Not_applied"
ERROR = "error", "Error"
IGNORED = "ignored", "Ignored"
UNPERMITTED = "unpermitted", "Unpermitted"
Status Flow
- PENDING - Delta uploaded, waiting for apply job
- STARTED - Apply job is processing this delta
- APPLIED - Successfully applied to dataset
- CONFLICT - Conflicting change detected
- ERROR - Application failed due to error
- UNPERMITTED - User lacks permission to apply
- IGNORED - Explicitly skipped during application
- NOT_APPLIED - Failed to apply for other reasons
See core/models.py:2109.
Structure
A deltafile is a JSON document containing multiple deltas:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project": "123e4567-e89b-12d3-a456-426614174000",
"version": "1.0",
"created": "2024-01-15T10:30:00Z",
"client": "QField 3.0",
"clientId": "device-uuid-here",
"deltas": [
{
"uuid": "delta-uuid-1",
"clientId": "device-uuid-here",
"localLayerId": "survey_points",
"localLayerName": "Survey Points",
"method": "create",
"new": {
"geometry": {...},
"attributes": {...}
}
},
{
"uuid": "delta-uuid-2",
"clientId": "device-uuid-here",
"localLayerId": "survey_lines",
"method": "patch",
"localPk": "456",
"old": {...},
"new": {...}
}
]
}
Validation
Deltafiles are validated against JSON schema:
utils.get_deltafile_schema_validator().validate(deltafile_json)
Validation checks:
- Required fields present
- Valid UUID formats
- Supported delta methods
- Valid geometry (if present)
- Project ID matches request
See core/views/deltas_views.py:78.
Uploading Deltas
API Endpoint
POST /api/v1/deltas/{projectid}/
Request
Multipart form data with deltafile:
curl -X POST https://app.qfield.cloud/api/v1/deltas/{projectid}/ \
-H "Authorization: Token YOUR_API_TOKEN" \
-F "file=@deltafile.json"
Upload Process
-
Receive Deltafile
- Extract from multipart request
- Parse JSON content
-
Validation
if deltafile_projectid != str(projectid):
raise exceptions.DeltafileValidationError()
if not project_obj.has_the_qgis_file:
raise exceptions.NoQGISProjectError()
-
Duplicate Detection
delta_ids = sorted([str(delta["uuid"]) for delta in deltas])
existing_delta_ids = [
str(v)
for v in Delta.objects.filter(id__in=delta_ids)
.order_by("id")
.values_list("id", flat=True)
]
-
Create Delta Objects
delta_obj = Delta(
id=delta["uuid"],
deltafile_id=deltafile_id,
project=project_obj,
content=delta,
client_id=delta["clientId"],
created_by=request.user,
)
-
Permission Check
if not permissions_utils.can_create_delta(request.user, delta_obj):
delta_obj.last_status = Delta.Status.UNPERMITTED
else:
delta_obj.last_status = Delta.Status.PENDING
-
Auto-Apply
if created_deltas:
jobs.apply_deltas(
project_obj,
request.user,
project_obj.the_qgis_file_name,
project_obj.overwrite_conflicts,
)
See core/views/deltas_views.py:67.
Apply Jobs
ApplyJob Model
class ApplyJob(Job):
deltas_to_apply = models.ManyToManyField(
to=Delta,
through="ApplyJobDelta",
)
overwrite_conflicts = models.BooleanField(
help_text="Automatically overwrite conflicts while applying deltas"
)
Apply jobs process batches of pending deltas.
See core/models.py:2403.
ApplyJobDelta Through Model
Tracks status of each delta within a job:
class ApplyJobDelta(models.Model):
apply_job = models.ForeignKey(ApplyJob, on_delete=models.CASCADE)
delta = models.ForeignKey(Delta, on_delete=models.CASCADE)
status = models.CharField(choices=Delta.Status.choices, default=Delta.Status.PENDING)
feedback = JSONField(null=True)
modified_pk = models.TextField(null=True)
See core/models.py:2426.
Apply Process
-
Job Creation
- Collect pending deltas
- Create ApplyJob instance
- Link deltas via ApplyJobDelta
-
Worker Execution
- QGIS worker container starts
- Opens project file
- Processes deltas sequentially
-
Per-Delta Application
- Validate delta structure
- Check for conflicts
- Apply to layer
- Record outcome
-
Completion
- Update delta statuses
- Save modified project files
- Update project timestamp
Conflict Detection
What Causes Conflicts?
Conflicts occur when:
-
Concurrent Edits
- Same feature edited on desktop and mobile
- Multiple mobile devices edit same feature
-
Stale Data
- Mobile package outdated
- Desktop changes not yet packaged
-
Deleted Features
- Feature deleted on desktop, edited on mobile
- Feature edited on desktop, deleted on mobile
Conflict Detection Logic
During delta application:
# For PATCH deltas
if delta.method == "patch":
current_feature = layer.getFeature(pk)
# Check if old values match current
if delta.old_attributes != current_feature.attributes():
# Conflict detected!
delta.last_status = Delta.Status.CONFLICT
Conflicts are detected by comparing:
- Delta’s
old values
- Current feature state
- If different, someone else made changes
Conflict Resolution
Automatic Resolution
When project.overwrite_conflicts=True:
if apply_job.overwrite_conflicts:
# Apply delta regardless of conflicts
# Latest change wins
apply_delta_to_feature(delta)
delta.last_status = Delta.Status.APPLIED
Best for:
- Field data collection workflows
- Single editor per feature
- Time-based priority (latest wins)
Manual Resolution
When project.overwrite_conflicts=False:
if conflict_detected and not apply_job.overwrite_conflicts:
delta.last_status = Delta.Status.CONFLICT
delta.last_feedback = {
"conflict_reason": "Feature modified since package creation",
"old_value": delta.old_attributes,
"current_value": current_feature.attributes(),
"new_value": delta.new_attributes,
}
Project manager must:
- Review conflicting deltas
- Decide which changes to keep
- Manually resolve via API or UI
- Re-trigger apply job
Resolution Strategies
- Last Write Wins - Automatic, simple
- Manual Review - Time-consuming, precise
- Attribute-Level - Merge non-conflicting attributes
- Time-Based - Prefer changes from specific time range
Delta Querying
List All Deltas
GET /api/v1/deltas/{projectid}/
Filter by Deltafile
GET /api/v1/deltas/{projectid}/deltafiles/{deltafileid}/
Status Summary
Get delta count by status:
@staticmethod
def get_status_summary(filters={}):
rows = (
Delta.objects.filter(**filters)
.values("last_status")
.annotate(count=Count("last_status"))
.order_by()
)
counts = {}
for status, _name in Delta.Status.choices:
counts[status] = rows_as_dict.get(status, 0)
return counts
See core/models.py:2161.
Geometry Handling
Geometry Fields
Deltas store geometries for spatial operations:
old_geom = models.GeometryField(null=True, srid=4326, dim=4)
new_geom = models.GeometryField(null=True, srid=4326, dim=4)
- SRID 4326 - WGS84 coordinate system
- 4D - Supports X, Y, Z, M dimensions
Use Cases
- Spatial queries on deltas
- Visualization of changes
- Conflict detection based on location
- Analysis of field coverage
Faulty Deltafiles
When deltafile upload fails:
class FaultyDeltaFile(models.Model):
deltafile_id = models.UUIDField(null=True)
deltafile = DynamicStorageFileField(upload_to=get_faulty_deltafile_upload_to)
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True)
user_agent = models.CharField(max_length=255)
traceback = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
Faulty deltafiles:
- Preserved for debugging
- Include error traceback
- Track client information
- Help identify systematic issues
See core/models.py:2679 and core/views/deltas_views.py:162.
Best Practices
Field Workflow
- Sync Frequently - Upload deltas at least daily
- Check Status - Monitor apply job outcomes
- Handle Conflicts - Review and resolve promptly
- Update Packages - Refresh mobile data regularly
Conflict Prevention
- Partition Work - Assign geographic areas to users
- Feature Locking - Coordinate who edits what
- Frequent Sync - Reduce time between updates
- Latest Packages - Download fresh data before fieldwork
- Batch Deltas - Upload in deltafiles, not individually
- Optimize Apply - Let automatic apply handle routine cases
- Monitor Jobs - Track apply job duration
- Clean Old Deltas - Archive applied deltas periodically
Troubleshooting
- Check Permissions - Verify user can create deltas
- Validate Schema - Ensure deltafile format correct
- Review Logs - Check apply job output
- Test Locally - Reproduce issues in QGIS
API Reference
Upload Deltafile
POST /api/v1/deltas/{projectid}/
Content-Type: multipart/form-data
Field: file containing deltafile JSON
List Deltas
GET /api/v1/deltas/{projectid}/
Trigger Apply (Deprecated)
POST /api/v1/deltas/{projectid}/apply/
Note: Use jobs endpoint instead:
{
"project_id": "uuid",
"type": "delta_apply"
}