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.
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.
Getting installed
omobile folder to /wp-content/plugins/.dbDelta — no data is written until setup is complete.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 );
19 database tables
All tables use the WordPress table prefix (default wp_). Created on activation via dbDelta.
| Table | Description |
|---|---|
omobile_devices | Registered mobile installs — platform, version, push token, locale, timezone |
omobile_sessions | Active app sessions with start/end timestamps |
omobile_api_log | Every REST request logged — method, path, status, duration |
omobile_telemetry | Raw event stream from the app |
omobile_telemetry_rollup | Daily rollup: date, platform, version, event name, count |
omobile_crashes | Crash reports with SHA-256 fingerprints and occurrence counters |
omobile_flags | Feature flags with type, value, rollout percentage |
omobile_announcements | In-app messages — banner, modal, toast — with expiry |
omobile_push_queue | Outbound push jobs awaiting cron dispatch |
omobile_content_health | Posts flagged for missing image, excerpt, or content |
omobile_snapshots | Point-in-time captures of flags + remote config |
omobile_webhooks | Registered outbound webhook URLs and their event subscriptions |
omobile_refresh_tokens | Active refresh token records with rotation chain state |
omobile_login_attempts | Per-identifier throttle event log |
omobile_segments | Device targeting rules in JSON |
omobile_audit | Admin action audit trail |
omobile_api_keys | API key store — prefix + SHA-256 hash only (raw key never persisted) |
omobile_app_versions | Version status and force-update rules per platform |
omobile_remote_config | Key/value remote config store |
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.
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.
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.
Login throttling
Failed login attempts are rate-limited per identifier (username/email) and per IP simultaneously.
| Threshold | Action | Duration |
|---|---|---|
| 5 failed attempts | Identifier locked | 15 minutes |
Implemented with WordPress transients. The omobile_login_attempts table records the events for the audit log; the transient drives the actual blocking logic.
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.
| Property | Detail |
|---|---|
| Format | omk_ prefix + 40 hex characters |
| Storage | SHA-256 hash + first 8 chars (prefix) only |
| Authentication | Authorization: Bearer omk_… |
| Rate limit | 120 req/min per key (configurable) |
| IP whitelist | Optional, one IP per line |
| Expiry | Optional expiry date |
BASHwp omobile create-api-key --name="My App"
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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/login | Public | Authenticate, receive JWT pair |
| POST | /auth/refresh | Public | Rotate refresh token |
| POST | /auth/logout | Bearer | Revoke refresh token |
| GET | /auth/me | Bearer | Current user profile |
Config endpoint
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /config | Bearer | App 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
| Method | Path | Description |
|---|---|---|
| POST | /devices/register | Register or update device (platform, version, push token, locale, timezone, model) |
| POST | /telemetry/events | Submit batch of named events with properties |
| POST | /telemetry/session | Start or end a session |
| POST | /crashes/report | Submit crash — message, stack trace, platform, version, OS |
| GET | /content/posts | Paginated 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.
| Method | Path | Description |
|---|---|---|
| GET POST DEL | /admin/flags | Feature flag CRUD |
| GET PUT | /admin/remote-config | Remote config CRUD |
| GET POST | /admin/push-queue | Push queue management |
| GET | /admin/segments | List segments |
| GET | /admin/audit | Audit log |
| GET POST DEL | /admin/api-keys | API key management |
| GET POST | /admin/app-versions | Version rule management |
| GET | /admin/snapshots | Snapshot list |
Feature flags
Key/value pairs returned under flags in the /config response. Support gradual rollouts down to individual devices.
| Type | Description |
|---|---|
boolean | true / false |
string | Arbitrary text value |
number | Integer or float |
json | Parsed 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.
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.
Push notifications
Three push drivers — choose one based on your app stack.
.p8 key file — no certificate renewal needed.Push payload fields
| Field | Description |
|---|---|
title | Notification title |
body | Notification body text |
image_url | Rich notification image |
deep_link | In-app navigation URL |
campaign_name | For analytics attribution |
ab_variant | A/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
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.
https://oauth2.googleapis.com/token, caching the resulting token for 55 minutes.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).
.p8 file — it is shown once only.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.
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.
| Field | Description |
|---|---|
version | App version string (e.g. 2.1.0) |
platform | ios, android, or all |
status | supported, deprecated, or blocked |
force_update | If true, app must update before continuing |
min_required | Minimum version still supported |
message | User-facing message shown on update prompt |
store_url | Link to App Store / Play Store |
The /config response includes a version_check object based on the requesting app's version and platform query params.
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.
Analytics methods
Computed from omobile_sessions, omobile_telemetry_rollup, and omobile_devices. All methods available from WP-CLI via wp omobile analytics.
| Method | Returns |
|---|---|
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 |
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
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" }
In-app announcements
Returned in the /config response. Displayed client-side as banners, modals, or toasts.
| Type | Description |
|---|---|
banner | Persistent top or bottom bar |
modal | Full-screen overlay |
toast | Ephemeral notification, auto-dismisses |
Announcements support title, body, an active flag, and an optional expires_at timestamp that auto-hides the announcement.
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
Outbound webhooks
Register webhook URLs to receive real-time HMAC-signed events from OMobile.
| Event | Fires when |
|---|---|
crash.new | New crash report received |
push.sent | Push notification dispatched successfully |
push.failed | Push delivery failure |
flag.updated | Feature flag value changed |
device.registered | New 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>
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.
BASHwp omobile snapshot --label="Before v2.1 release"
Onboarding wizard
Guides new installations through 6 steps. Progress stored in wp_options under omobile_onboarding_step.
| Step | Description |
|---|---|
welcome | Intro screen |
jwt | Generate and store a JWT secret |
test-connection | Verify REST API is reachable from the internet |
push-config | Configure FCM / APNS / Expo credentials |
test-push | Send a test push notification |
done | Setup complete — dashboard unlocked |
PHP// Reset the wizard (also available in Setup tab)
OMobile_Onboarding::reset();
Demo mode
One-click demo data seeding for exploring the plugin without a connected app.
| Seeded data | Count |
|---|---|
| Registered devices (iOS + Android mix) | 20 |
| Feature flags | 4 |
| Remote config keys | 3 |
| In-app announcements | 1 |
| API log entries | 50 |
| Crash reports | 5 |
| App version rules | 2 |
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.
WP-CLI commands
All commands use the wp omobile prefix.
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,
}),
);
}
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
| Group | Tabs |
|---|---|
| Overview | Dashboard, Setup |
| App | Devices, Segments, Analytics, Versions |
| Push | Push, Announcements |
| Backend | Flags, Remote Config |
| Monitoring | API Monitor, Crashes, Telemetry, Content Health |
| Auth | Auth, API Keys, Audit Log |
| System | Settings, Webhooks, Snapshots, Demo, Diagnostics, SDK Docs |
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 );
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
omobile_* tables.omobile_* options from wp_options.manage_omobile capability from all users and deletes the omobile_manager role.omobile_* cron events.omobile_* transients.define( 'OMOBILE_KEEP_DATA_ON_UNINSTALL', true ); to wp-config.php before deleting the plugin.Got a question about OMobile?
Reach out directly — Kenneth replies within 24 hours.
