Content Sync Client Flow
This is the recommended end-to-end flow for a client that stores public content locally.
Full Flow
Store one sync token per canonical resource filter. Do not reuse a token for a different filter. The API canonicalizes resource filters by group and ID; duplicate IDs are deduped and IDs are sorted before a token is created.
Applying Changes
Every change belongs to one resource, identified by:
resource_group, such asmushafs,translations,word_by_word_transliterations, orchapter_recitationsresource_id, such as Mushaf ID1, translation resource content ID19, word-by-word transliteration resource60, orAudio::Recitation.id159for a chapter recitation
Row-level changes also include:
record_type, such astranslation,word_translation,word_transliteration, orchapter_audio_filerecord_key, the row key inside that resource
Client Algorithm
| Change type | Client action |
|---|---|
RESOURCE_CREATE | Fetch snapshot_url, then replace all local rows for that resource. |
RESOURCE_INVALIDATE | Fetch snapshot_url, then replace all local rows for that resource. |
RESOURCE_DELETE | Remove or hide the full local resource. The resource is no longer public. |
ROW_CREATE | Insert the row from data, or replace it if it already exists. |
ROW_UPDATE | Replace the local row with data. |
ROW_DELETE | Delete that one local row. |
RESOURCE_UPDATE | Keep existing rows. Treat it as a freshness marker only. |
Pagination Rules
The sync endpoint can return multiple pages. While has_more is true, use next_page_url. Do not build the cursor yourself.
Only the final page has the token your app should store for the next sync.
Token Rules
| Rule | Why it matters |
|---|---|
| Store the token after the final page only. | Earlier pages do not represent a complete sync. |
| Store the token with the canonical resource filter. | Tokens are bound to the normalized filter they were created for. |
| Replace the old token after each completed sync. | The latest token is your new checkpoint. |
| If token recovery fails, bootstrap again. | A fresh bootstrap rebuilds local state from current public content. |
Minimal Local State
interface ContentSyncState {
resourcesFilter: string;
syncToken: string;
lastSuccessfulSyncAt: string;
}
interface LocalContentRow {
resourceGroup:
| "articles"
| "mushafs"
| "recitations"
| "chapter_recitations"
| "tafsirs"
| "translations"
| "word_by_word_translations"
| "word_by_word_transliterations";
resourceId: number;
recordType: string;
recordKey: string;
data: Record<string, unknown>;
}
The current sync contract accepts only articles, chapter_recitations, mushafs, recitations, tafsirs, translations, word_by_word_translations, and word_by_word_transliterations as resource groups.
word_by_word_transliterations:60 uses word_transliteration rows. Each row
contains exactly id, resource_content_id, resource_id, word_id,
language_id, language_name, text, and updated_at. Here
resource_id == resource_content_id == ResourceContent.id identifies the
owning resource; word_id identifies the Word owner. Keep this
group separate from the translation-only word_by_word_translations group.
recitations remains the legacy ayah-by-ayah group and is keyed by
Recitation.id. chapter_recitations is keyed by Audio::Recitation.id and
contains chapter_audio_file rows. When a chapter recitation shares resource
content with a legacy ayah recitation, compatible chapter-file mutations may
also appear in the legacy resource for existing clients.
For the full schema, see Sync public content resources.