Skip to main content
The installation API stores app identity, user bindings, native tokens, permission, consent, capabilities, and app activity. Use the React Native SDK for Expo and bare application integration, and the push delivery API for provider configuration and sends. The same public contracts serve self-hosted Galinum and Galinum Cloud.

Bootstrap and authentication

Generate an installation ID and a cryptographically random capability on the device. Persist both before sending a request. Use at least 32 random bytes, encoded as base64url, for the capability. Keep it in secure device storage. Send POST /api/v1/sdk/installations with the project publishable key:
Replaying this request with the same capability and app identity returns the installation. A different capability or app identity returns 409. The server stores a verifier and never returns the capability. IDs are scoped to a project. For subsequent SDK reads and mutations, supply both headers:
GET /api/v1/sdk/installations/{installationId} returns current state. App ID, platform and environment cannot change after bootstrap.

Binding and revision guards

First identify the user through POST /api/v1/identify. Then send PUT /api/v1/sdk/installations/{installationId}/binding:
Use the external user ID. Set userId to null to unbind. A changed binding increments its generation, clears consent and clears the previous user’s activity. The installation retains its token. User identity follows the existing identify trust model; the installation capability does not prove the customer’s login. All mutations require a new requestId and the current revision and bindingGeneration. Successful mutations increment revision. Replaying the same operation and body returns its original acknowledgement. A changed replay body or stale revision returns 409 without changing state. The server retains only the latest 128 successful mutation acknowledgements per installation. Replays do not refresh that window. After eviction, replaying the original stale-revision request returns 409; it cannot apply the mutation again. No request tombstone remains after eviction. Use fresh request IDs for new operations. An acknowledgement describes the original result, not current state. This also applies after token retirement or an account switch. Always use the GET route for current state instead of restoring state from an old acknowledgement. Persist logout locally before sending the unbind. Keep messaging disabled while remote acknowledgement is pending. Capture the binding generation when callbacks are created. Never relabel an old callback with the new user’s generation.

Facts, tokens, and app activity

Each suffix follows /api/v1/sdk/installations/{installationId}. Each body also contains the mutation guards above. permission is unknown, not_determined, denied, provisional or granted. Consent is a separate boolean. Capabilities contain actions and channels string arrays, plus richImages as a boolean. Token revisions increase independently from binding generations. Native tokens are scoped by project, app, environment and provider: APNs for iOS, FCM for Android. Registering the same scoped token on another installation atomically removes it from the old owner. Captured token revisions fence later invalid-token reports. Only explicit app use records activity. Token refresh, permission synchronization and other background callbacks must not call the activity endpoint. These routes never request OS permission themselves. Installation request bodies are limited to 262,144 encoded bytes (256 KiB), including JSON escapes and whitespace. The schema permits 32 unique action IDs of up to 64 Unicode characters and 64 unique channel IDs of up to 128 Unicode characters. Optional categories reports up to 16 iOS categories, each with an ID (128 code points) and up to four ordered { id, title } action entries (64 code points each). The byte budget accommodates compact JSON at those maxima, including escaped supplementary Unicode characters. Bodies over the budget return 413 without changing state. Character limits count Unicode code points, not UTF-16 code units.

Inspection and eligibility

Use a project secret with GET /api/v1/installations. Optional query parameters are userId (external ID), page (default 1), and perPage (default 25, maximum 100). Results contain installations, total count and pagination. Neither this response nor SDK reads include capabilities’ secret, verifier, native token or token digest. Reported action/channel capabilities remain visible. The public selectInstallations rule in @galinum/core accepts last_active, all, or specific selection. Eligibility requires the identified user, a token, product consent, and granted or provisional permission. Last-active selection uses app activity, then installation ID to break ties. A specific ineligible device returns no selection. These rules do not dispatch a campaign. Wire types and validation schemas are generated from OpenAPI in @galinum/contracts. This package has no Node imports or server dependencies.

In-app decisions and feedback

In-app messaging uses the project publishable key and the same external user ID as identification. It does not require an installation capability or push consent. Native and web use web_inapp campaigns and the same project/user/campaign delivery and variant assignment. Send GET /api/v1/messages with userId, entryId, requestId, and path query parameters. Generate entry/request IDs before the request. A native screen path such as /exports follows the same campaign pages rules as a web pathname. The response echoes the three identities alongside messages and uses Cache-Control: no-store. Compare them against the current confirmed user, route entry, and local facts before selecting a message. Cached content never supplies fresh display authority. For an actual presentation, send POST /api/v1/deliveries/{id}/event:
Types are shown, clicked, dismissed, and converted. Each operation needs a durable, stable feedbackId; exact retries reuse it. A different render gets a new ID. A canonical HTTP 200 response contains userId, deliveryId, type, receiptId equal to the feedback ID, and the first acknowledgedAt timestamp. Validate that tuple before marking local work acknowledged. Conflicting reuse returns 409. Terminal feedback requires a previously recorded shown operation. SDKs also enforce a durable shown predecessor for the same local entry. Store feedback with its original user and delivery before sending. Never relabel it after logout. Locally completed messages stay suppressed during retries. Acknowledged completion suppresses a later server decision on another device, including between web and native. Concurrent authorized entries can still race: there is no global display lease or recall of already-painted content. See React Native rendering. These are framework-independent HTTP contracts. A Swift or Kotlin integration can implement them without Expo or a React runtime. It must preserve revision replays, captured identity, ordered observations, fresh entry decisions, and durable feedback receipts. React Native bridge classes are not standalone Swift or Kotlin SDKs.

Upgrade an existing self-host database

Fresh databases use packages/server/schema.sql. Existing databases need the one-time packages/server/upgrades/installations.sql upgrade before the upgraded server handles installation requests. Updating the Compose image does not rerun fresh-volume initialization. Back up your database and stop the API workers. From the product repository root, run the following with your existing database’s DDL connection:
The SQL file wraps both new tables and the index in one transaction. It leaves existing projects, users, campaigns and deliveries unchanged. Restart the server only after the command succeeds. Check installation inspection through the secret-key GET /api/v1/installations route. Run this upgrade once on a database without the installation tables. It fails instead of silently accepting pre-existing or incompatible tables. A failed transaction does not leave a partial upgrade. Databases initialized from the current schema.sql already contain these tables and must skip the upgrade. schema.sql remains canonical. Contributors regenerate this specific upgrade with node scripts/generate-installation-upgrade.mjs; verification rejects drift.