Developers

Developer documentation

This section covers everything a developer needs to integrate a mobile app with the content system, understand the data model, and manage releases.

System architecture

The content system has two separate APIs:

Admin Backend (content-backend.vanilla.nl) – REST API for the admin UI. Handles content CRUD, user auth (Google OAuth), snapshots, imports, and translations. Not used by mobile apps.

Client API (content-api.vanilla.nl) – Content delivery API for mobile apps. Uses HMAC-SHA256 authentication and AES-256-GCM response encryption. Serves published snapshot data based on app version, locale, and platform.

The two APIs share a PostgreSQL database (with pgvector for semantic search) and Redis for caching and nonce replay protection.

Data model

SchemaType (defines statement structure)
  └── App (linked to one schema type)
       ├── Categories (filter-based statement grouping)
       ├── Statements (localized content items)
       └── Snapshots (immutable content packages)
            └── AppVersionRelease (maps app version → snapshot)

Key relationships:

  • Statements belong to a schema type, not an app. Multiple apps sharing the same schema can access the same statements.
  • Categories belong to an app and use JSON filters to select statements dynamically.
  • Snapshots belong to an app and freeze the resolved state of all categories + statements at generation time.
  • Version mappings connect a semver app version to a published snapshot. The client API uses these to resolve which content to serve.

Key areas

  • API Integration – Complete client API reference: HMAC authentication, AES-256-GCM encryption, endpoints, version resolution, and Flutter integration example
  • Schemas – Defining statement structure with parts, join strings, and literals
  • Snapshots – Snapshot lifecycle, payload structure, and version mapping
  • Releasing – Step-by-step release checklist for shipping content
  • CSV Import – Bulk importing statements from CSV files

API Integration

Client API Reference

The client API at content-api.vanilla.nl serves published content to mobile apps. All requests require HMAC-SHA256 authentication. Content responses are encrypted with AES-256-GCM.

This document covers everything needed to build a client that fetches content from the API.


Authentication (HMAC-SHA256)

Every request must include signed headers. The server validates the signature, timestamp, and nonce before processing.

Required headers

HeaderFormatDescription
Vanilla-TimestampUnix seconds (integer string)Current time. Must be within ±300s of server time.
Vanilla-NonceUUID v4Unique per request. Rejected if reused within 5 minutes.
Vanilla-App-VersionSemver stringApp version (e.g., 1.2.3). Used for snapshot resolution.
Vanilla-App-BuildStringBuild number (e.g., 42).
Vanilla-App-OSStringPlatform identifier: iOS or Android.
Vanilla-App-LanguageLocale codeRequested language (e.g., en, de). Lowercase.
Vanilla-SignatureHex stringHMAC-SHA256 of the canonical string.

You must also include at least one of:

Schemas

Schema types

A schema type defines the text structure of statements. Every statement belongs to exactly one schema type, and every app is linked to one schema type. The schema determines which text parts a statement has and how they combine into full displayed text.

Structure

A schema’s structure is a JSON object with three fields:

Parts

An ordered array of named text segments. Each regular part becomes a localized text field on every statement. Parts wrapped in {} are literals – static text with fixed translations.

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:

Releasing

Release checklist

Step-by-step process for shipping content to mobile apps.

Before you start

Make sure you have at least the CONTENT role (for creating snapshots) or DEV role (for the full workflow including schema changes).


1. Verify content

  • Check statement counts per schema type – do the numbers match what you expect?
  • Verify translations are complete for all target locales (check the Translations tab for missing entries)
  • Confirm all statements that should be active are marked active
  • Review platform visibility flags – are the right statements hidden on iOS/Android?

2. Check category filters

  • For each category, click the test button to preview which statements match
  • Verify the matched statement count makes sense
  • Check that no category is empty (0 matches) unless intentional
  • Remember: filters are resolved at snapshot time, so what you see in the test is what the snapshot will contain

3. Create snapshot

  1. Go to Release ManagerCreate Snapshot
  2. Select the target app
  3. Add a descriptive comment (e.g., “v1.3 content: 50 new party questions, updated translations”)
  4. Click Generate to build the payload

4. Preview and verify

After generation, check:

CSV Import

CSV import

The Import Data tab handles bulk importing statements from CSV files. The import process uses AI to transform your CSV data into the correct schema structure.

Import steps

The import follows a three-step flow shown by the progress indicator at the top:

Step 1: Upload and configure

  1. Select Target App – Choose which app the statements will be imported into
  2. Select Schema Type – Pick the schema that matches your content structure. Once selected, the schema’s parts are shown as a preview so you can verify your CSV matches
  3. Upload CSV – Drag and drop a .csv file into the upload zone, or click to browse. The file must be a valid CSV

Step 2: Preview

After upload, the system sends your CSV to the backend where AI analyzes and transforms the data to match the selected schema’s parts structure. The preview shows:

UI Strings

UI Strings — Live App Translations

The UI strings system manages interface translations for all PsycatGames apps. Strings are organized by key, translated into 22 languages, and served to apps via a live API. Apps can pull updated translations without an app store release.


How It Works

  1. Keys are created in the content tool (e.g., generalPlay, settingsTitle)
  2. Each key has translations in 22 locales (en, de, fr, es, etc.)
  3. Apps fetch translations at runtime via the client API
  4. Apps bundle a build-time snapshot as offline fallback
  5. At runtime, the app checks the API for newer translations and caches locally

Client API Endpoint

GET https://content-api.vanilla.nl/v1/ui-strings?locale=de

Authentication: Same HMAC-SHA256 as all other endpoints (see API Integration).