Getting Started

First-time setup

This walks through setting up a new app from scratch and publishing content to mobile clients. You’ll need at least the DEV role to create schemas and apps.

1. Create a Schema Type

Go to Schema Types in the sidebar. Click Create Schema.

A schema defines the text structure of your statements. It has:

  • Parts – an ordered list of named text segments. Regular parts (like prefix, option1) are user-editable text fields. Parts in {} (like {OR}) are literals with fixed translations.
  • Join string – character(s) used to combine parts into full text (usually a space)
  • Literals – static text with translations per locale, used for connector words

Example: A “Would You Rather” schema with parts ["title", "text1", "text2"] and join " " produces statements like “Would you rather eat a bug drink sour milk” (the app handles displaying the “or” between options).

Under the hood: The schema is stored as a JSON structure. Every statement created for an app using this schema must provide localized text for each regular part. The schema is also embedded in every snapshot so mobile clients know how to reconstruct full text from parts.

2. Create an App

Go to App Management. Click Create App.

  • Give it a name (e.g., “Truth or Dare”)
  • Select the schema type you just created
  • Add bundle IDs to link it to your iOS/Android apps (e.g., com.psycatgames.truthordare)

Under the hood: The app’s schema type is permanent – it’s enforced on every statement import and creation. Bundle IDs are how the client API maps incoming requests to the correct app (the mobile client sends its bundle ID in the Vanilla-App-Bundle header).

3. Add statements

You can add statements in two ways:

  • Manual – Go to All Statements, click Create Statement. Fill in the text parts for each locale according to the schema
  • CSV Import – Go to Import Data, select the target app and schema, upload a CSV file. The system uses AI to transform your CSV columns into the correct schema parts

Each statement gets:

  • Text parts (localized per language)
  • A mode (e.g., “classic”, “truth”, “dare”) for gameplay categorization
  • Tags for additional organization
  • Platform visibility flags (hidden on iOS/Android)
  • Optional metadata blocks (designer, content, game)

Under the hood: Statements belong to a schema type, not directly to an app. Multiple apps can share the same schema type and access the same pool of statements. Vector embeddings are generated for semantic search and category matching.

4. Organize with categories

Go to Categories and create categories for your app. Each category has:

  • A name, emoji, and localized title/description
  • A JSON filter that determines which statements belong to it
  • Platform visibility flags (show/hide on iOS or Android)
  • Paid/explicit flags

Categories use filter rules to match statements – you don’t manually assign statements to categories. Filters can match on mode, tags, metadata fields, or even semantic similarity.

Under the hood: Filters are stored as JSON on the category. They’re not evaluated live in the app – they’re resolved at snapshot generation time into concrete lists of statement IDs. This means:

  • Adding new statements after creating a snapshot won’t affect that snapshot
  • To include new statements, you need to create a new snapshot
  • You can test category filters at any time to see what they currently match

5. Create a snapshot and publish

Go to Release Manager. Click Create Snapshot and select the app.

  1. Draft – Snapshot is created. Click Generate to resolve all category filters into statement lists and build the payload.
  2. Ready – Mark the snapshot as ready for release. The payload is locked.
  3. Published – Snapshot is immutable and available via the content API. A SHA-256 checksum and byte size are calculated.

After publishing, go to the App Releases tab to map specific app versions to your snapshot.

What happens when you publish

The full chain from publish to mobile app:

  1. Snapshot published – the payload is locked with a checksum. It contains: all active statements (with localized parts), all active categories (with their resolved statement ID lists), app info (name, bundle IDs, schema), and metadata (locale list, counts).

  2. Version mapping created – you assign the snapshot to app version “1.2.0”. Multiple versions can point to the same snapshot.

  3. Mobile app requests content – the app sends its bundle ID, version, language, and platform in signed headers to content-api.vanilla.nl.

  4. Version resolution – the API finds the best matching snapshot: exact version match first, then highest version ≤ requested. If the app is version 1.3.0 but only 1.2.0 has a mapping, it gets 1.2.0’s snapshot.

  5. Content filtering – statements and categories are filtered by platform (iOS/Android visibility flags). Localized text is resolved: requested locale → English fallback → first available.

  6. Encryption – the response is encrypted with AES-256-GCM and returned as binary. The IV is sent in the Vanilla-Enc-Nonce header.

  7. Caching – encrypted responses are cached in Redis for 4 hours. Subsequent identical requests get a cache hit without re-processing.

Next steps

  • API Integration – implement content fetching in your mobile app
  • Snapshots – snapshot lifecycle, version mapping, and rollback
  • Releasing – step-by-step release checklist