Stores Domain - Signals Documentation
Source: api/nextango/apps/stores/signals.py
Last Updated: 2026-07-09
Overview
Stores signals cover four concerns: slug derivation before save, outbound Sanity sync for Store, TaxJurisdiction, and ComplianceCheck, response-cache invalidation, and inventory backfill for newly created stores.
There is no Site signal. The Site outbound sync signal was removed in the store-as-url minimal reversal; Site is Django-owned and does not sync to Sanity in either direction (signals.py:201-205).
derive_store_slug_on_save
Trigger: pre_save on Store. Synchronous.
Source: signals.py:25-52
Guarantees every store has a URL-routable slug before it is saved. When slug is empty, derives one from the store name (falling back to store_code, then a short unique token) via stores.slug_utils.derive_store_slug, checking uniqueness against the live table; the DB unique constraint is the backstop under concurrency. An already-set slug is respected, so the derivation only fills missing values.
Because this runs on pre_save, it covers every creation path that goes through Model.save(): ORM, admin, inbound Sanity sync via serializer.save(), and data migrations using the real model. Bulk paths that bypass save() (bulk_create, QuerySet.update) are not covered, but no store creation path uses them.
sync_store_to_sanity
Trigger: post_save on Store. Synchronous (the sync call itself runs in-request through the sync orchestrator).
Source: signals.py:55-132
Pushes Store changes to Sanity in real time through the integrations sync framework (SyncOrchestrator with the store sync handler). Layered loop-prevention guards ensure a save that originated from an inbound Sanity webhook is not echoed back to Sanity: the thread-local sync context, an instance-level sync-source marker, webhook-processing and in-progress-sync flags, and rapid-successive-save detection. Load-testing mode (LOAD_TESTING_MODE=true) disables the sync entirely. Sync failures are logged and never block the save.
What the outbound document contains, including the top-level regulatedCompliance block sourced from the store's compliance profile, is defined by the store sync handler; see below.
Outbound compliance block
The outbound store document always emits regulatedCompliance with four agnostic keys, sourced from store.compliance_profile (integrations/sync/store_sync.py:538-552):
{
"requireAgeGate": false,
"requiredCredentialTypes": [],
"secondaryReviewCredentialTypes": [],
"permits": []
}
The block is always present (defaults when no profile exists) so Sanity patch-set semantics clear stale values when a profile is removed. The store.site reference and slug are intentionally not emitted (store_sync.py:554-555).
delete_store_from_sanity
Trigger: post_delete on Store. Synchronous.
Source: signals.py:135-165
Outbound Sanity deletion is disabled by design (P13 deletion-semantics standard): deleting a Store in Django logs the event but does not delete the Sanity document. Store lifecycle deletion is expected to originate in Sanity, not Django.
sync_taxjurisdiction_to_sanity
Trigger: post_save on TaxJurisdiction. Synchronous.
Source: signals.py:168-198
TaxJurisdiction has no standalone outbound document sync; it is referenced from Store. When a jurisdiction changes (for example, rates updated), the handler re-fires post_save for every store using that jurisdiction, causing each store to re-sync to Sanity with the updated tax information. The same webhook-origin guard applies, so inbound jurisdiction updates do not trigger the cascade.
invalidate_store_cache
Trigger: post_save and post_delete on Store. Synchronous.
Source: signals.py:214-219
Invalidates the cached /stores/ list responses whenever a store is created, updated, or deleted, so the 600-second response cache never serves stale store data after a change. See Core - Caching.
sync_compliance_check_to_sanity
Trigger: post_save on ComplianceCheck. Asynchronous: defers via transaction.on_commit, then dispatches a Celery task.
Source: signals.py:226-256
Syncs the check outbound to Sanity as a complianceCheck document. Deferring to after commit avoids partial-write races; the actual push happens in a background task, so API writes are not blocked by Sanity latency. Webhook-originated saves are skipped by the sync-context guard.
reconcile_new_store_inventory_on_create
Trigger: post_save on Store, create only. Asynchronous: defers via transaction.on_commit, then dispatches a Celery task.
Source: signals.py:263-286
When a store is created, queues a chunked backfill of InventoryLevel records for available-everywhere product variants, so a store created after products were published is correctly stocked instead of appearing empty. Runs only on create; updates never re-stock. Deferring to after commit ensures the task never races the uncommitted Store row.
Related Documentation
- Models
- Serializers for the inbound (webhook) side
- Core - Caching
- Integrations for the sync framework