Snapshots

Snapshots and version mapping

Snapshots are immutable content packages. They capture all statements and categories for an app at a point in time, resolve category filters into concrete statement lists, and serve as the payload delivered to mobile clients.

What a snapshot contains

The payload is a single JSON blob stored in the database:

{
  "app": {
    "id": "uuid",
    "name": "Truth or Dare",
    "bundleIds": ["com.psycatgames.truthordare"],
    "contentSchemaType": {
      "id": 1,
      "name": "Truth or Dare",
      "structure": { "parts": ["main"], "join": " " }
    }
  },
  "categories": [
    {
      "id": "uuid",
      "name": "party-classics",
      "isActive": true,
      "hiddenOnIOS": false,
      "hiddenOnAndroid": false,
      "isPaid": false,
      "isExplicit": false,
      "emoji": "🎉",
      "designerMetadata": {},
      "gameMetadata": {},
      "localizedTexts": [
        { "locale": "en", "title": "Party Classics", "description": "..." },
        { "locale": "de", "title": "Party-Klassiker", "description": "..." }
      ],
      "statements": ["stmt-id-1", "stmt-id-2", "stmt-id-3"]
    }
  ],
  "statements": [
    {
      "id": "stmt-id-1",
      "mode": "classic",
      "isActive": true,
      "hiddenOnIOS": false,
      "hiddenOnAndroid": false,
      "tags": ["party"],
      "designerMetadata": {},
      "gameMetadata": {},
      "parts": [
        { "partName": "main", "locale": "en", "text": "What's your biggest fear?" },
        { "partName": "main", "locale": "de", "text": "Was ist deine größte Angst?" }
      ]
    }
  ],
  "metadata": {
    "generatedAt": "2025-01-15T10:30:00.000Z",
    "locales": ["de", "en", "es", "fr"],
    "categoryCount": 5,
    "statementCount": 250
  }
}

What’s excluded from snapshots:

  • Vector embeddings (only used for search/matching in the admin)
  • Filter definitions (resolved into statement ID lists at generation time)
  • Content metadata like fullText (reconstructed from parts + schema on the client)

Snapshot lifecycle

Every snapshot goes through three states:

Draft

Initial state after creation. The payload is empty until you click Generate.

Generate resolves all category filters into concrete statement lists:

  1. For each active category, the filter is evaluated against the statement database
  2. Matching statement IDs are stored in the category’s statements array
  3. All referenced statements are collected with their localized parts
  4. Statement and category metadata is validated against the app’s metadata schemas – items that fail validation are excluded (with warnings)
  5. The payload, checksum, byte size, and locale list are computed

You can regenerate a Draft snapshot to pick up content changes. Once moved to Ready, regeneration is locked.

Ready

Staging state. The payload is frozen – no regeneration allowed. Use this for review and preview before going live.

Move to Ready when you’re confident the content is correct. This is a one-way transition.

Published

Final, immutable state. The snapshot is:

  • Locked – cannot be modified or deleted
  • Available via the client API for any version mapping pointing to it
  • Timestamped with publishedAt
  • Checksummed (SHA-256) for integrity verification

Version mapping

The App Releases tab maps app versions (semver) to published snapshots. Each mapping has:

  • App – the target app
  • App Version – a semver string (e.g., 1.2.0, 2.0.0)
  • Snapshot – a published snapshot

Constraint: each (app, version) pair maps to exactly one snapshot. But multiple versions can point to the same snapshot.

Version fallback

When a mobile app requests content, the API resolves the snapshot:

  1. Find all version mappings for the app
  2. Try exact match against the requested version
  3. If no exact match, find the highest mapped version ≤ the requested version
  4. If nothing matches, return 404

This means you don’t need a mapping for every app version. If you map version 1.0.0 to a snapshot, all app versions from 1.0.0 up to (but not including) the next mapped version will receive that snapshot.

Rollback

To roll back content for a specific app version:

  1. Go to App Releases
  2. Edit the version mapping
  3. Point it to the previous published snapshot

The change takes effect immediately (after the 4-hour Redis cache expires, or after the cache key rotates). No need to create a new snapshot.

When to create a new snapshot

Create a new snapshot when:

  • You’ve added, removed, or edited statements since the last snapshot
  • Category filters have changed
  • You want different content for a new app version
  • Metadata schema changes require re-validation

You don’t need a new snapshot when:

  • You’re just updating version mappings (pointing existing versions to existing snapshots)
  • You’re rolling back to a previous snapshot

Snapshot table

The Release Manager shows all snapshots with:

  • App name and comment
  • Status (Draft / Ready / Published)
  • Category count and statement count
  • Number of active version mappings pointing to this snapshot
  • Payload size (bytes)
  • Locales included
  • Created and published timestamps