Developer documentation

OMobile v1.0.0

Turns your WordPress site into a full-featured mobile app backend — JWT auth, remote config, feature flags, push notifications, crash reporting, analytics, and more.

WordPress plugin WP 6.0 · PHP 8.0+ Namespace: omobile/v1 v1.0.0
01 · Overview

What OMobile does

A complete mobile backend as a WordPress plugin — 19 database tables, a full REST API, three push drivers, and an analytics suite. No external service accounts needed beyond your push provider credentials.

🔐
JWT Authentication
HS256, refresh token rotation, reuse detection, 15-min access tokens
🚩
Feature Flags
Boolean, string, number, JSON — per-device rollout percentages
⚙️
Remote Config
Server-controlled key/value pairs, no app deploy needed
🔔
Push Notifications
FCM HTTP v1, APNS token-based (p8), Expo push API
📱
App Versioning
Force-update, deprecation, and blocking rules per platform
💥
Crash Reporting
SHA-256 fingerprint grouping, deduplication, resolution tracking
📊
Analytics
DAU, D1/D7/D30 retention cohorts, top screens, crash rate
🎯
Segments
Target pushes by platform, version, locale, timezone
📣
Announcements
Banners, modals, and toasts delivered via the config endpoint
🔗
Webhooks
HMAC-signed outbound events for crash, push, flags, devices
📷
Config Snapshots
Point-in-time flag + remote config captures for rollback
⌨️
WP-CLI
Full CLI for all operations — seed, rollup, push, analytics
02 · Installation

Getting installed

1
Upload the omobile folder to /wp-content/plugins/.
2
Activate via Plugins → Installed Plugins. All 19 database tables are created via dbDelta — no data is written until setup is complete.
3
Navigate to OMobile in the WordPress admin menu.
4
Complete the Setup Wizard (Onboarding tab) — generate JWT secret, verify REST API, configure push credentials.

wp-config.php constant

PHP// Override the JWT secret (takes precedence over the wp_options stored secret)
define( 'OMOBILE_JWT_SECRET', 'your-256-bit-secret-here' );

// Keep all data when deleting the plugin
define( 'OMOBILE_KEEP_DATA_ON_UNINSTALL', true );
03 · Database Tables

19 database tables

All tables use the WordPress table prefix (default wp_). Created on activation via dbDelta.

TableDescription
omobile_devicesRegistered mobile installs — platform, version, push token, locale, timezone
omobile_sessionsActive app sessions with start/end timestamps
omobile_api_logEvery REST request logged — method, path, status, duration
omobile_telemetryRaw event stream from the app
omobile_telemetry_rollupDaily rollup: date, platform, version, event name, count
omobile_crashesCrash reports with SHA-256 fingerprints and occurrence counters
omobile_flagsFeature flags with type, value, rollout percentage
omobile_announcementsIn-app messages — banner, modal, toast — with expiry
omobile_push_queueOutbound push jobs awaiting cron dispatch
omobile_content_healthPosts flagged for missing image, excerpt, or content
omobile_snapshotsPoint-in-time captures of flags + remote config
omobile_webhooksRegistered outbound webhook URLs and their event subscriptions
omobile_refresh_tokensActive refresh token records with rotation chain state
omobile_login_attemptsPer-identifier throttle event log
omobile_segmentsDevice targeting rules in JSON
omobile_auditAdmin action audit trail
omobile_api_keysAPI key store — prefix + SHA-256 hash only (raw key never persisted)
omobile_app_versionsVersion status and force-update rules per platform
omobile_remote_configKey/value remote config store
04 · Authentication

Auth overview

Two authentication methods: JWT (for mobile users) and API Keys (for server-to-server and CI). All mobile endpoints also require the X-Om-Install-Id header.

Access token TTL
15m
Refresh token TTL
30d
Reuse window
30s
Lock after failures
5×
then 15 min lockout
4.1 · JWT Flow

JWT authentication

OMobile uses HS256 JWTs — pure PHP with base64url encoding, no external library.

Login

HTTPPOST /wp-json/omobile/v1/auth/login
Content-Type: application/json
X-Om-Install-Id: <your-device-uuid>

{
  "username": "user@example.com",
  "password": "secret"
}
RESPONSE{
  "access_token":  "eyJ…",
  "refresh_token": "eyJ…",
  "user": {
    "id":           1,
    "display_name": "Jane Doe",
    "email":        "user@example.com"
  }
}

Authenticated requests

HTTPAuthorization: Bearer <access_token>
X-Om-Install-Id: <your-device-uuid>

Token refresh

HTTPPOST /wp-json/omobile/v1/auth/refresh
Content-Type: application/json

{ "refresh_token": "eyJ…" }

Returns a new access_token and a rotated refresh_token.

4.2 · Refresh Tokens

Refresh token rotation

Refresh tokens use a rotation + reuse detection pattern designed to catch token theft in real time.

  • Each refresh issues a new token and invalidates the old one immediately.
  • A 30-second reuse window tolerates race conditions where two requests use the same token near-simultaneously.
  • If a consumed token is presented outside the reuse window, the entire chain is revoked — all sessions for that user on that device are ended immediately, indicating the old token was stolen and replayed.
4.3 · Login Throttling

Login throttling

Failed login attempts are rate-limited per identifier (username/email) and per IP simultaneously.

ThresholdActionDuration
5 failed attemptsIdentifier locked15 minutes

Implemented with WordPress transients. The omobile_login_attempts table records the events for the audit log; the transient drives the actual blocking logic.

4.4 · API Keys

API keys

An alternative to JWT for server-to-server or CI integrations. Only the SHA-256 hash is ever stored — the raw key is shown once at creation and never again.

PropertyDetail
Formatomk_ prefix + 40 hex characters
StorageSHA-256 hash + first 8 chars (prefix) only
AuthenticationAuthorization: Bearer omk_…
Rate limit120 req/min per key (configurable)
IP whitelistOptional, one IP per line
ExpiryOptional expiry date
BASHwp omobile create-api-key --name="My App"
05 · REST API Reference

REST API reference

Base URL: https://yoursite.com/wp-json/omobile/v1/. All mobile endpoints require X-Om-Install-Id with a unique per-install UUID.

Auth endpoints

MethodPathAuthDescription
POST/auth/loginPublicAuthenticate, receive JWT pair
POST/auth/refreshPublicRotate refresh token
POST/auth/logoutBearerRevoke refresh token
GET/auth/meBearerCurrent user profile

Config endpoint

MethodPathAuthDescription
GET/configBearerApp config, flags, remote config, announcements, version check

Cached per platform+version for 60 seconds. Cache invalidated automatically whenever a flag or remote config is updated. Query params: ?platform=ios|android and ?version=2.1.0.

RESPONSE{
  "flags": {
    "new_checkout_flow": {
      "value": true, "rollout_pct": 50, "in_rollout": true
    }
  },
  "remote_config": {
    "min_cart_value": 10,
    "support_email": "help@example.com"
  },
  "announcements": [],
  "version_check": {
    "status": "supported", "force_update": false, "message": ""
  }
}

Device, telemetry & crash endpoints

MethodPathDescription
POST/devices/registerRegister or update device (platform, version, push token, locale, timezone, model)
POST/telemetry/eventsSubmit batch of named events with properties
POST/telemetry/sessionStart or end a session
POST/crashes/reportSubmit crash — message, stack trace, platform, version, OS
GET/content/postsPaginated posts feed
GET/content/posts/{id}Single post

Admin REST endpoints

Require a logged-in admin or a valid API key with manage_omobile capability.

MethodPathDescription
GET POST DEL/admin/flagsFeature flag CRUD
GET PUT/admin/remote-configRemote config CRUD
GET POST/admin/push-queuePush queue management
GET/admin/segmentsList segments
GET/admin/auditAudit log
GET POST DEL/admin/api-keysAPI key management
GET POST/admin/app-versionsVersion rule management
GET/admin/snapshotsSnapshot list
06 · Feature Flags

Feature flags

Key/value pairs returned under flags in the /config response. Support gradual rollouts down to individual devices.

TypeDescription
booleantrue / false
stringArbitrary text value
numberInteger or float
jsonParsed JSON object or array

Rollout percentage

Set rollout_pct to 1–100. Each device is assigned a random number (1–100) on first config fetch. If the device's number ≤ rollout_pct, the flag appears in their response as in_rollout: true. A flag at 10% reaches approximately 10% of devices, gradually ratcheting up.

Cache invalidation

When a flag is toggled or updated, OMobile_REST_Config::invalidate_cache() deletes all omobile_config_* transients. The next request regenerates the cache fresh.

07 · Remote Config

Remote config

Arbitrary key/value pairs returned under remote_config in the /config response. Change values without a new app release.

  • Feature toggles without code deploys
  • Store URLs, support emails, deep-link URIs
  • Minimum cart values, discount thresholds
  • Any server-controlled constant

Supports the same 4 types as feature flags: string, number, boolean, json.

08 · Push Notifications

Push notifications

Three push drivers — choose one based on your app stack.

FCM HTTP v1
Android apps + Firebase iOS. Uses service account JSON + OAuth2 token exchange.
APNS Token-based
Native iOS apps. Uses .p8 key file — no certificate renewal needed.
Expo Push
React Native / Expo apps. No credentials — routes via the Expo Push API.

Push payload fields

FieldDescription
titleNotification title
bodyNotification body text
image_urlRich notification image
deep_linkIn-app navigation URL
campaign_nameFor analytics attribution
ab_variantA/B testing variant label

Push queue

Pushes are queued in omobile_push_queue and dispatched every minute by the omobile_dispatch_push cron event. To dispatch immediately:

BASHwp omobile dispatch-push
8.1 · FCM v1 Setup

FCM HTTP v1

OMobile uses the FCM HTTP v1 API — not the deprecated legacy server key. Credentials are exchanged for an OAuth2 access token and cached for 55 minutes.

1
In Firebase Console → Project Settings → Service Accounts → Generate New Private Key — download the JSON file.
2
In OMobile → Settings → FCM: paste the full JSON content and your Firebase project ID.
3
The plugin exchanges the service account for an RS256 JWT → https://oauth2.googleapis.com/token, caching the resulting token for 55 minutes.
8.2 · APNS Token-Based

APNS token-based

Uses .p8 key files — token-based auth never expires like certificates do. The ES256 JWT is cached for 55 minutes (Apple allows up to 1 hour).

1
In Apple Developer Portal → Certificates, Identifiers & Profiles → Keys → Create new key.
2
Enable Apple Push Notifications service (APNs) and download the .p8 file — it is shown once only.
3
Note your Team ID (top-right in developer portal) and the Key ID.
4
In OMobile → Settings → APNS: paste the p8 key content, team ID, key ID, and your app's bundle ID.
8.3 · Expo Push

Expo push setup

Select Expo as the push driver. No credentials needed — OMobile posts to the Expo Push API at https://exp.host/--/api/v2/push/send.

Devices must use the expo-notifications SDK and register their Expo push token via POST /devices/register.

09 · App Versioning & Force Update

Versioning & force update

Define version rules in the Versions tab. Each rule controls how the app behaves based on the version and platform reported in the /config request.

FieldDescription
versionApp version string (e.g. 2.1.0)
platformios, android, or all
statussupported, deprecated, or blocked
force_updateIf true, app must update before continuing
min_requiredMinimum version still supported
messageUser-facing message shown on update prompt
store_urlLink to App Store / Play Store

The /config response includes a version_check object based on the requesting app's version and platform query params.

10 · Crash Reporting & Grouping

Crash reporting & grouping

Crashes are submitted by the app and de-duplicated server-side using SHA-256 fingerprinting — duplicate crashes never create new rows.

Crash report payload

JSONPOST /omobile/v1/crashes/report
{
  "message":     "NullPointerException in CartScreen",
  "stack_trace": "...",
  "platform":    "android",
  "app_version": "2.1.0",
  "os_version":  "14"
}

Fingerprinting

The fingerprint is a SHA-256 hash of the first 3 lines of the normalised stack trace. When a crash matches an existing fingerprint, only count and last_seen_at are updated — no new row is inserted.

Resolution

Crashes can be marked resolved in the Crashes tab. Resolved crashes optionally store a GitHub issue URL for traceability.

11 · Analytics

Analytics methods

Computed from omobile_sessions, omobile_telemetry_rollup, and omobile_devices. All methods available from WP-CLI via wp omobile analytics.

MethodReturns
OMobile_Analytics::dau( $days )Daily Active Users for last N days
OMobile_Analytics::retention()D1 / D7 / D30 retention cohorts
OMobile_Analytics::top_screens( $limit )Most-visited screens
OMobile_Analytics::top_events( $limit )Most-fired events
OMobile_Analytics::platform_split()iOS vs Android device count
OMobile_Analytics::version_distribution()Devices per app version
OMobile_Analytics::avg_session_duration()Mean session length in seconds
OMobile_Analytics::crash_rate(){ rate_pct, crashes, sessions } for last 30 days
12 · Telemetry

Event telemetry

Apps submit events in batches. Raw events are rolled up daily into omobile_telemetry_rollup.

JSONPOST /omobile/v1/telemetry/events
{
  "events": [
    { "name": "product_view", "properties": { "sku": "ABC123" } },
    { "name": "add_to_cart",  "properties": { "sku": "ABC123", "qty": 2 } }
  ]
}

Each rollup row contains: date, platform, app_version, event_name, event_count. Run the rollup manually:

BASHwp omobile rollup
13 · Devices & Segments

Devices & segments

Device registration

Every app install gets a unique install ID (UUID v4, generated on first launch) passed with every request via X-Om-Install-Id. Device records track: platform, app version, OS version, device model, push token, locale, timezone, last seen, active status.

Segments

Segments are JSON rule objects used to target push notifications to a subset of devices.

JSON{ "platform": "ios" }
{ "platform": "android", "min_app_version": "2.0.0" }
{ "locale": "en-US" }
14 · Announcements

In-app announcements

Returned in the /config response. Displayed client-side as banners, modals, or toasts.

TypeDescription
bannerPersistent top or bottom bar
modalFull-screen overlay
toastEphemeral notification, auto-dismisses

Announcements support title, body, an active flag, and an optional expires_at timestamp that auto-hides the announcement.

15 · Content Health

Content health

A cron-scheduled checker that flags WordPress posts with content issues — surfaced in the Content Health tab with direct edit links.

  • Missing featured image
  • Empty excerpt
  • Empty post content
16 · Webhooks

Outbound webhooks

Register webhook URLs to receive real-time HMAC-signed events from OMobile.

EventFires when
crash.newNew crash report received
push.sentPush notification dispatched successfully
push.failedPush delivery failure
flag.updatedFeature flag value changed
device.registeredNew device registered for the first time

Payload & signature

JSON{
  "event":     "crash.new",
  "timestamp": "2026-04-25T10:00:00Z",
  "data": { ... }
}

Each request includes an X-OMobile-Signature header for verification:

HTTPX-OMobile-Signature: sha256=<HMAC-SHA256 of raw body using webhook secret>
17 · Snapshots

Config snapshots

Snapshots capture the current state of all feature flags and remote config at a point in time. Use them as a safety net before releases.

1
Create a "before" snapshot before deploying new flag configuration.
2
Deploy the new flag/config changes.
3
If something breaks, restore flags manually to the snapshot state from the admin UI.
BASHwp omobile snapshot --label="Before v2.1 release"
18 · Onboarding Wizard

Onboarding wizard

Guides new installations through 6 steps. Progress stored in wp_options under omobile_onboarding_step.

StepDescription
welcomeIntro screen
jwtGenerate and store a JWT secret
test-connectionVerify REST API is reachable from the internet
push-configConfigure FCM / APNS / Expo credentials
test-pushSend a test push notification
doneSetup complete — dashboard unlocked
PHP// Reset the wizard (also available in Setup tab)
OMobile_Onboarding::reset();
19 · Demo Mode

Demo mode

One-click demo data seeding for exploring the plugin without a connected app.

Seeded dataCount
Registered devices (iOS + Android mix)20
Feature flags4
Remote config keys3
In-app announcements1
API log entries50
Crash reports5
App version rules2
BASHwp omobile seed-demo   # seed demo data
wp omobile clear-demo  # remove all demo data

Both operations are also available in the admin UI under the Demo tab.

20 · WP-CLI Reference

WP-CLI commands

All commands use the wp omobile prefix.

# Show plugin status and diagnostics wp omobile status   # Generate + store a new JWT secret wp omobile generate-secret   # Create an API key wp omobile create-api-key --name="My App"   # Demo data wp omobile seed-demo wp omobile clear-demo   # Run telemetry rollup immediately wp omobile rollup   # Delete logs beyond retention period wp omobile sweep   # Dispatch pending push notifications immediately wp omobile dispatch-push   # Print analytics summary (last 30 days) wp omobile analytics   # Create a config snapshot wp omobile snapshot --label="Before release"
21 · SDK Quick Start

SDK quick start

Copy-paste integration for the two most common mobile stacks. Store tokens in secure storage — never in plain AsyncStorage or shared preferences.

JSimport * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
import Constants from 'expo-constants';

const BASE_URL  = 'https://yoursite.com/wp-json/omobile/v1/';
const INSTALL_ID = Constants.installationId; // stable UUID per install

async function authHeaders() {
  const token = await SecureStore.getItemAsync('access_token');
  return {
    'Authorization': `Bearer ${token}`,
    'X-Om-Install-Id': INSTALL_ID,
    'Content-Type': 'application/json',
  };
}

// Login
export async function login(username, password) {
  const res  = await fetch(`${BASE_URL}auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Om-Install-Id': INSTALL_ID },
    body: JSON.stringify({ username, password }),
  });
  const data = await res.json();
  if (data.access_token) {
    await SecureStore.setItemAsync('access_token',  data.access_token);
    await SecureStore.setItemAsync('refresh_token', data.refresh_token);
  }
  return data;
}

// Get config (flags + remote config)
export async function getConfig() {
  const res = await fetch(
    `${BASE_URL}config?platform=${Platform.OS}&version=${Constants.expoConfig.version}`,
    { headers: await authHeaders() }
  );
  return res.json();
}

// Register device + push token
export async function registerDevice(pushToken) {
  const res = await fetch(`${BASE_URL}devices/register`, {
    method: 'POST',
    headers: await authHeaders(),
    body: JSON.stringify({
      platform:    Platform.OS,
      app_version: Constants.expoConfig.version,
      push_token:  pushToken,
      locale:      'en-US',
      timezone:    Intl.DateTimeFormat().resolvedOptions().timeZone,
    }),
  });
  return res.json();
}

// Submit telemetry events
export async function trackEvents(events) {
  await fetch(`${BASE_URL}telemetry/events`, {
    method: 'POST',
    headers: await authHeaders(),
    body: JSON.stringify({ events }),
  });
}

// Report crash
export async function reportCrash(error) {
  await fetch(`${BASE_URL}crashes/report`, {
    method: 'POST',
    headers: await authHeaders(),
    body: JSON.stringify({
      message:     error.message,
      stack_trace: error.stack,
      platform:    Platform.OS,
      app_version: Constants.expoConfig.version,
      os_version:  Platform.Version.toString(),
    }),
  });
}
DARTimport 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:convert';
import 'dart:io';

const baseUrl  = 'https://yoursite.com/wp-json/omobile/v1/';
final _storage = FlutterSecureStorage();
const installId = 'YOUR-UUID-HERE'; // generate once, store persistently

Future<Map<String, String>> authHeaders() async {
  final token = await _storage.read(key: 'access_token') ?? '';
  return {
    'Authorization': 'Bearer $token',
    'X-Om-Install-Id': installId,
    'Content-Type': 'application/json',
  };
}

// Login
Future<Map<String, dynamic>> login(String username, String password) async {
  final res = await http.post(
    Uri.parse('${baseUrl}auth/login'),
    headers: { 'Content-Type': 'application/json', 'X-Om-Install-Id': installId },
    body: jsonEncode({ 'username': username, 'password': password }),
  );
  final data = jsonDecode(res.body) as Map<String, dynamic>;
  if (data['access_token'] != null) {
    await _storage.write(key: 'access_token',  value: data['access_token']);
    await _storage.write(key: 'refresh_token', value: data['refresh_token']);
  }
  return data;
}

// Get config
Future<Map<String, dynamic>> getConfig(String version) async {
  final platform = Platform.isIOS ? 'ios' : 'android';
  final res = await http.get(
    Uri.parse('${baseUrl}config?platform=$platform&version=$version'),
    headers: await authHeaders(),
  );
  return jsonDecode(res.body);
}

// Register device
Future<void> registerDevice(String pushToken, String version) async {
  final platform = Platform.isIOS ? 'ios' : 'android';
  await http.post(
    Uri.parse('${baseUrl}devices/register'),
    headers: await authHeaders(),
    body: jsonEncode({ 'platform': platform, 'app_version': version, 'push_token': pushToken }),
  );
}

// Track events
Future<void> trackEvents(List<Map<String, dynamic>> events) async {
  await http.post(
    Uri.parse('${baseUrl}telemetry/events'),
    headers: await authHeaders(),
    body: jsonEncode({ 'events': events }),
  );
}

// Report crash
Future<void> reportCrash(Object error, StackTrace stack) async {
  final platform = Platform.isIOS ? 'ios' : 'android';
  await http.post(
    Uri.parse('${baseUrl}crashes/report'),
    headers: await authHeaders(),
    body: jsonEncode({
      'message':     error.toString(),
      'stack_trace': stack.toString(),
      'platform':    platform,
    }),
  );
}
22 · Admin UI

Admin UI

Accessed via OMobile in the WordPress admin menu. Single page at admin.php?page=omobile — no WP sidebar sub-items.

Layout

  • Row 1 — Brand bar: OMobile logo + name + version badge + light/dark toggle
  • Row 2 — Nav: Horizontal scrollable tabs grouped by dividers

Theme preference is persisted in localStorage as omobile_theme. Dark mode adds the class omobile-dark to <body>.

Navigation groups

GroupTabs
OverviewDashboard, Setup
AppDevices, Segments, Analytics, Versions
PushPush, Announcements
BackendFlags, Remote Config
MonitoringAPI Monitor, Crashes, Telemetry, Content Health
AuthAuth, API Keys, Audit Log
SystemSettings, Webhooks, Snapshots, Demo, Diagnostics, SDK Docs
23 · Filters & Actions

Filters & actions

Filters

PHP// Override the storage adapter (default: transients)
add_filter( 'omobile_storage_adapter', function( $adapter ) {
    return new My_Redis_Adapter(); // must implement OMobile_Storage_Interface
} );

// Modify config payload before it's returned to the app
add_filter( 'omobile_config_payload', function( $payload, $install_id ) {
    $payload['custom_key'] = 'custom_value';
    return $payload;
}, 10, 2 );

// Control which capability manages OMobile (default: 'manage_omobile')
add_filter( 'omobile_admin_cap', function() {
    return 'manage_options';
} );

Actions

PHP// Fires after a crash is reported
add_action( 'omobile_crash_reported', function( $crash_id, $data ) {
    // notify Slack, create GitHub issue, etc.
}, 10, 2 );

// Fires after a push notification is dispatched
add_action( 'omobile_push_dispatched', function( $queue_id, $result ) {
    // log result, update campaign stats, etc.
}, 10, 2 );

// Fires after a feature flag is updated
add_action( 'omobile_flag_updated', function( $flag_key, $new_value ) {
    // trigger downstream cache invalidation, etc.
}, 10, 2 );
24 · Uninstall & Cleanup

Uninstall & cleanup

Deactivating leaves all data intact for a clean reactivation. Deleting the plugin via the WordPress UI runs uninstall.php and removes everything.

What uninstall.php removes

1
Drops all 19 omobile_* tables.
2
Deletes all omobile_* options from wp_options.
3
Removes the manage_omobile capability from all users and deletes the omobile_manager role.
4
Deletes all omobile_* cron events.
5
Removes all omobile_* transients.
💡To keep all data on deletion, add define( 'OMOBILE_KEEP_DATA_ON_UNINSTALL', true ); to wp-config.php before deleting the plugin.
✦ Need help?

Got a question about OMobile?

Reach out directly — Kenneth replies within 24 hours.