inverge / nexus
Official PHP SDK for the Inverge Nexus platform — realtime, events, errors, logs, feature flags, links, sessions. Framework-agnostic with first-class Laravel and Symfony integrations.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- monolog/monolog: ^3.0
- phpunit/phpunit: ^10.5
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Suggests
- illuminate/support: Laravel integration (auto-discovered service provider + facade).
- monolog/monolog: Forward your app's logs to Nexus via Inverge\Nexus\Monolog\NexusLogHandler (^3).
- psr/http-client: Bring your own PSR-18 HTTP client (e.g. Guzzle) instead of the built-in cURL transport.
- symfony/http-kernel: Symfony integration (bundle + auto error-capture listener).
- symfony/messenger: Symfony async telemetry delivery (nexus: async: true).
Provides
None
Conflicts
None
Replaces
None
README
inverge/nexus is the server‑side client for the Inverge Nexus platform:
realtime messaging, product analytics, structured logging, error monitoring,
sessions, feature flags, remote config, deep‑link attribution, and surveys —
from plain PHP, Laravel, or Symfony.
- Package:
inverge/nexus - PHP:
>= 8.1
Table of contents
- Installation
- Plain PHP setup
- Laravel setup
- Symfony setup
- Dispatchers (sync vs queued)
- Sessions
- Events
- Logs
- Errors
- Feature flags
- Remote Config
- Deep links & attribution
- Realtime
- Surveys
- Push notifications
- In‑app messages
- Live Activities
- Monolog handler
- Low‑level request
1. Installation
composer require inverge/nexus
2. Plain PHP setup
use Inverge\Nexus\NexusClient; use Inverge\Nexus\Config; $nexus = new NexusClient(new Config( apiKey: 'nxs_live_xxx', baseUrl: 'https://services.inverge.net', // optional timeout: 10.0, // optional (seconds) defaultHeaders: [], // optional )); $nexus->events()->capture('order_placed', ['total' => 42.0], ['distinctId' => 'user_1']);
Config fields: apiKey (required), baseUrl, timeout, defaultHeaders.
Config::fromArray('nxs_…', [...]) is also available.
Resources: realtime(), events(), errors(), logs(), sessions(),
flags(), links(), surveys(), remoteConfig(), push(), inApp(),
liveActivities(). Plus config() and request().
Switching services off
Turn a Nexus product off without removing the code that uses it — e.g. stop Live Activities with one environment variable. A switched-off service makes no network call (and queues no job) and never throws: writes are skipped, reads return their neutral value (flags → off, Remote Config → your default, lists → empty), and the Laravel channels skip it. All default to on.
NEXUS_LIVE_ACTIVITIES_ENABLED=false # just Live Activities NEXUS_ENABLED=false # everything (e.g. local / CI)
| Variable | services key |
Switches off |
|---|---|---|
NEXUS_REALTIME_ENABLED |
realtime |
rooms / emits |
NEXUS_EVENTS_ENABLED |
events |
events |
NEXUS_ERRORS_ENABLED |
errors |
error capture |
NEXUS_LOGS_ENABLED |
logs |
logs |
NEXUS_SESSIONS_ENABLED |
sessions |
sessions |
NEXUS_FLAGS_ENABLED |
flags |
feature flags (→ your defaults) |
NEXUS_REMOTE_CONFIG_ENABLED |
remote_config |
Remote Config (→ your defaults) |
NEXUS_LINKS_ENABLED |
links |
deep links |
NEXUS_SURVEYS_ENABLED |
surveys |
surveys |
NEXUS_PUSH_ENABLED |
push |
push notifications |
NEXUS_IN_APP_ENABLED |
in_app |
in-app messages |
NEXUS_LIVE_ACTIVITIES_ENABLED |
live_activities |
Live Activities |
false, 0, off, no mean off. Plain PHP reads them from the environment;
Laravel reads them through config/nexus.php (enabled + services), so
php artisan config:cache keeps working. In code:
Config::fromArray($key, ['services' => ['push' => false]]) (unknown names are
rejected), and $nexus->enabled(Services::PUSH) tells you whether one is on.
3. Laravel setup
The service provider auto‑registers. Publish config if you want to tweak it:
php artisan vendor:publish --tag=nexus-config
.env:
NEXUS_API_KEY=nxs_live_xxx NEXUS_BASE_URL=https://services.inverge.net NEXUS_TIMEOUT=10 NEXUS_QUEUE=false # true → dispatch telemetry on the queue NEXUS_QUEUE_CONNECTION= NEXUS_QUEUE_NAME= NEXUS_LOGGING=false # true → forward Laravel logs to Nexus NEXUS_LOG_LEVEL=debug NEXUS_CAPTURE_ERRORS=true # report unhandled exceptions automatically
Use the facade anywhere:
use Inverge\Nexus\Laravel\Nexus; Nexus::realtime()->emit('orders:42', 'status', ['state' => 'shipped']); Nexus::events()->capture('order_placed', ['total' => 42], ['distinctId' => auth()->id()]); Nexus::remoteConfig()->all(['userProperties' => ['governorate' => 'Erbil']]);
Queued variant (offloads the HTTP call to a job):
app('nexus.queue')->events()->capture('order_placed', ['total' => 42], ['distinctId' => $userId]);
Unhandled exceptions are reported automatically when NEXUS_CAPTURE_ERRORS=true.
Notification channels
Send Nexus push notifications, Live Activities and realtime messages from
ordinary Laravel notifications — one method per product, like toMail():
| Method | Returns | Own channel |
|---|---|---|
toNexusPush($notifiable) |
NexusPushMessage — same API as $nexus->push()->notification() |
'nexus-push' |
toNexusLiveActivity($notifiable) |
NexusLiveActivityMessage — start / update / end |
'nexus-live-activity' |
toNexusRealtime($notifiable) |
NexusRealtimeMessage — room emits, events, logs, errors |
'nexus-realtime' |
Put NexusChannel::class (or 'nexus') in via() and every method the
notification has is sent — no need to list each product. Or list the products'
own channels to choose per notifiable (e.g. only push for users who opted in).
Tell Nexus who the notifiable is (its distinctId, the id your apps pass to
Nexus.identify):
class User extends Authenticatable { use Notifiable; public function routeNotificationForNexus(): string { return "CLIENTS.{$this->id}"; } }
Then each order notification maps to one Live Activity step, plus a push:
use Inverge\Nexus\Laravel\Notifications\NexusChannel; use Inverge\Nexus\Laravel\Notifications\NexusLiveActivityMessage; use Inverge\Nexus\Laravel\Notifications\NexusPushMessage; class OrderStatusChanged extends Notification implements ShouldQueue { use Queueable; public function __construct(public Order $order) {} public function via(object $notifiable): array { return [NexusChannel::class]; // push + Live Activity below } public function toNexusPush(object $notifiable): NexusPushMessage { return NexusPushMessage::create('Your fuel is on the way', "Driver: {$this->order->driver_name}") ->data(['screen' => "/orders/{$this->order->id}"]); } public function toNexusLiveActivity(object $notifiable): NexusLiveActivityMessage { $id = "order_{$this->order->id}"; return match ($this->order->status) { 'Pending' => NexusLiveActivityMessage::create($id)->start('order_tracking') // a template from the console ->step('pending')->etaAt($this->order->eta) ->values(['driver' => $this->order->driver_name, 'quantity' => "{$this->order->liters} L"]), 'Completed' => NexusLiveActivityMessage::create($id)->end()->step('completed'), 'Canceled' => NexusLiveActivityMessage::create($id)->end()->canceled(), default => NexusLiveActivityMessage::create($id)->update()->step($this->order->status), // partial, merged }; } }
- A push or a Live Activity start goes to the notifiable's route unless the
message targets someone itself (
->toUser(),->toSegment(),->toAll()/->shared()). Updates and ends address the activity by id, so they need no route. Returnnullfrom ato…method to skip that channel. - Per-product routes override the shared one:
routeNotificationForNexusPush(),routeNotificationForNexusLiveActivity(),routeNotificationForNexusRealtime(). A route may be a distinctId, a list of them, or['distinctId' => …]. - On-demand:
Notification::route('nexus', 'CLIENTS.7')->notify(new OrderStatusChanged($order)); NexusLiveActivityMessage::create($id)->startActivity($type)starts a plain activity for your own iOS widget instead of a template.NexusChannelis safe to queue. It sends each product independently (one failing doesn't stop the others), still fails the job so Laravel retries it, and remembers — in your cache, keyed by the notification's id — what already went out, so a retry re-sends only what failed, never a second push. A product whose own channel is also invia()is left to that channel, so it's never sent twice.- Use the products' own channels when you want
via()to pick per notifiable, orNotificationSent/NotificationFailedper product. toNexus()/NexusMessage(the realtime method's and message's former names) still work.
4. Symfony setup
Register the bundle and configure it:
# config/packages/nexus.yaml nexus: api_key: '%env(NEXUS_API_KEY)%' base_url: 'https://services.inverge.net'
Inject NexusClient via autowiring; an exception subscriber reports uncaught
exceptions, and a Messenger handler is available for async dispatch.
5. Dispatchers (sync vs queued)
The client sends requests through a dispatcher. Default is synchronous cURL.
Swap it for a queued one (Laravel QueueDispatcher, Symfony
MessengerDispatcher) so telemetry never blocks the request:
$queued = $nexus->withDispatcher($myDispatcher); // returns a new client
You can also inject a custom PSR‑18 transport: new NexusClient($config, $psr18Transport).
6. Sessions
// Identify an end‑user: $nexus->sessions()->identify('user_123', [ 'email' => 'a@b.com', 'name' => 'Ada', 'traits' => ['plan' => 'pro'], ]); // Start/refresh a session: $nexus->sessions()->track([ 'distinctId' => 'user_123', 'deviceKey' => 'dev_abc', 'sessionKey' => 'sess_abc', 'country' => 'IQ', ]);
Returns the server payload (incl. sessionId).
7. Events
// One event → returns the number written (0/1): $nexus->events()->capture('order_placed', ['total' => 42.0, 'currency' => 'USD'], [ 'distinctId' => 'user_1', 'sessionKey' => 'sess_1', ]); // A batch of events sharing one identity/context: $nexus->events()->batch([ ['name' => 'view', 'properties' => ['sku' => 'A1']], ['name' => 'click', 'properties' => ['sku' => 'A1']], ], ['distinctId' => 'user_1']);
capture(string $name, array $properties = [], array $context = []): int.
Context keys: distinctId, sessionKey, deviceKey, release, osType, osVersion, browser, appVersion, timestamp.
8. Logs
$nexus->logs()->info('payment started', ['source' => 'checkout']); $nexus->logs()->error('charge failed', ['context' => ['code' => 'declined']]); // Generic + all levels: trace, debug, info, warn, error, fatal $nexus->logs()->log('warn', 'retrying', ['context' => ['attempt' => 2]]); // Batch: $nexus->logs()->batch([ ['level' => 'info', 'message' => 'a'], ['level' => 'error', 'message' => 'b'], ], ['distinctId' => 'user_1']);
Each level method: info(string $message, array $options = []): int. Options
include source, context, and the usual identity keys.
9. Errors
// From a caught throwable (recommended): try { doWork(); } catch (\Throwable $e) { $nexus->errors()->captureException($e, [ 'handled' => true, 'level' => 'error', 'context' => ['feature' => 'checkout'], 'distinctId' => 'user_1', ]); } // Or a manual error: $nexus->errors()->capture('Payment gateway timeout', [ 'type' => 'GatewayTimeout', 'level' => 'error', 'fingerprint' => 'gateway-timeout', ]);
Options: type, level, handled, fingerprint, stack, context, release, url, distinctId, sessionKey, deviceKey, osType, osVersion, browser, appVersion.
10. Feature flags
$nexus->flags()->isEnabled('new_checkout', 'user_1', ['plan' => 'pro']); // bool $nexus->flags()->variant('paywall', 'user_1'); // ?string $nexus->flags()->payload('paywall', 'user_1'); // mixed $all = $nexus->flags()->evaluate('user_1', ['plan' => 'pro']); // full result
evaluate(string $distinctId, array $properties = []): array returns every
flag; the others are convenience wrappers.
11. Remote Config
Fetch the active, published template resolved for a context (conditions — platform, version, country, percentile, custom attributes — evaluated server‑side).
// Flat key => value map: $config = $nexus->remoteConfig()->all([ 'appVersion' => '2.1.0', 'country' => 'IQ', 'userProperties' => ['governorate' => 'Duhok'], ]); $phone = $config['phone_number']; // A single value with a default: $phone = $nexus->remoteConfig()->get('phone_number', '+9640000000000', [ 'userProperties' => ['governorate' => 'Erbil'], ]); // Full result (version, etag, per‑parameter value + which condition supplied it): $result = $nexus->remoteConfig()->fetch( ['userProperties' => ['governorate' => 'Duhok']], $previousEtag, // optional If-None-Match; result['notModified'] === true when unchanged ); // $result['parameters']['phone_number'] === ['value' => ..., 'valueType' => 'STRING', 'source' => 'Duhok']
Context keys: appInstanceId, appVersion, appBuild, platform, osVersion, country, language, firstOpenTime, userProperties. userProperties values must
be primitives (strings/numbers/bools) and match condition values exactly.
fetch(array $context = [], ?string $etag = null): arrayall(array $context = []): arrayget(string $key, mixed $default = null, array $context = []): mixed
12. Deep links & attribution
$data = $nexus->links()->attribute('install', [ 'clickId' => 'abc', 'name' => 'summer_sale', 'platform' => 'ios', 'distinctId' => 'user_1', 'properties' => ['campaign' => 'promo'], ]);
attribute(string $type, array $options = []): array. Types: install, open,
reengagement, … Options include name, clickId, distinctId, deviceId, sessionKey, platform, osType, country, revenue, properties.
13. Realtime
Server‑to‑client fan‑out and room management over the data plane.
use Inverge\Nexus\RoomMessage; // Emit one or more events to a room: $nexus->realtime()->emit('orders:42', 'status', ['state' => 'shipped']); $nexus->realtime()->emit('orders:42', ['status', 'updated'], ['state' => 'shipped']); // Emit with a RoomMessage value object: $nexus->realtime()->emit(new RoomMessage('orders:42', ['status'], ['state' => 'shipped'])); // Batch many messages: $nexus->realtime()->broadcast([ new RoomMessage('room:a', ['ping'], ['n' => 1]), new RoomMessage('room:b', ['ping'], ['n' => 2]), ]); // Same payload to several rooms: $nexus->realtime()->emitToRooms(['a', 'b'], 'ping', ['n' => 1]); // Room management: $nexus->realtime()->registerRoom('orders', 'standard'); $rooms = $nexus->realtime()->rooms(); $nexus->realtime()->deleteRoom($roomId); $nexus->realtime()->related('orders'); // Link / unlink related rooms: $nexus->realtime()->link($roomId, $relatedId); $nexus->realtime()->unlink($roomId, $relatedId); // Payload schema (validation): $nexus->realtime()->setSchema($roomId, ['type' => 'object', 'required' => ['state']]); $nexus->realtime()->enableSchema($roomId, true); $nexus->realtime()->schema($roomId); $nexus->realtime()->clearSchema($roomId);
14. Surveys
// Surveys a user is eligible for (targeting/sampling/capping applied): $surveys = $nexus->surveys()->active([ 'distinctId' => 'user_1', 'properties' => ['plan' => 'pro'], 'osType' => 'ios', ]); // Submit a response (answers keyed by question id): $nexus->surveys()->respond('survey_1', ['q1' => 9, 'q2' => 'Great'], [ 'completed' => false, 'distinctId' => 'user_1', ]); // Convenience wrappers: $nexus->surveys()->complete('survey_1', ['q1' => 9], ['distinctId' => 'user_1']); $nexus->surveys()->dismiss('survey_1', ['distinctId' => 'user_1']);
15. Push notifications
Send transactional push to users or segments across their registered devices. Campaigns, provider credentials, segments and templates live in the console; the server SDK is for one‑off, event‑driven sends.
Fluent builder (recommended):
$nexus->push()->notification() ->title('Your order shipped') ->body('Track it in the app') ->image('https://cdn.example.com/box.png') ->data(['screen' => '/orders/42']) ->button('track', 'Track', ['url' => 'https://x/track']) ->iosBadge(1) ->androidVisibility('public') ->setSegments(['vip', 'active_7d']) // union of saved segments ->send(); // → ['sent' => .., 'failed' => .., 'recipients' => ..]
Targeting (combine freely; union):
->toUsers(['user_1', 'user_2']) // or ->toUser('user_1') ->setSegments(['vip']) // aliases: ->toSegments([...]) / ->toSegment('vip') ->where(['platform' => 'ios', 'lang' => 'en']) // ad-hoc device filter (incl. tags) ->toAll() // everyone (overrides other targets)
Options — action buttons plus per-platform extras: iosBadge,
iosRelevanceScore, iosInterruptionLevel, iosSubtitle, androidVisibility,
androidLargeIcon, androidBigPicture, androidAccentColor, and option() /
options() for anything else (web icon/image/badge, small icon, …).
Quick send (no builder):
$nexus->push()->send(['user_1'], ['title' => 'Welcome 👋']); $nexus->push()->sendToUser('user_1', ['title' => 'Welcome 👋']);
16. In‑app messages
In‑app messages are composed in the console and shown by the client SDK. Server‑ side you can fetch the ones a user is eligible for (headless / server‑driven UIs) and record impressions / clicks.
$messages = $nexus->inApp()->active(['distinctId' => 'user_1']); $nexus->inApp()->impression('msg_1', ['distinctId' => 'user_1']); $nexus->inApp()->click('msg_1', 'cta', ['distinctId' => 'user_1']); $nexus->inApp()->dismiss('msg_1', ['distinctId' => 'user_1']);
17. Live Activities
A live, updating view of an in‑progress event on the iOS Lock Screen / Dynamic Island and as an Android 16 Live Update — driven from your backend. Start when the event begins, update as its status changes, end when it's done.
Templates (recommended)
Design the activity once in the Nexus console (Live Activities → Templates: layout, colours, steps, icons, translations). The Nexus mobile SDKs render it on iOS and Android, each device in its own language — your server sends only data:
$nexus->liveActivities()->template('order_tracking', 'order_42') // template key + your id ->step('pending') // a step id (or label) ->etaAt(new DateTimeImmutable('+25 minutes')) // live countdown on the device ->values(['driver' => 'Ahmed', 'quantity' => '500 L · Diesel']) // {{placeholders}} ->toUser('user_1') ->start(); // Updates are partial — only what changed; the server merges it. $nexus->liveActivities()->activity('order_42')->step('on_the_way')->progress(0.4)->update(); $nexus->liveActivities()->activity('order_42')->step('completed')->end(); // final state $nexus->liveActivities()->activity('order_42')->canceled()->end(); // or canceled
Data setters: step, progress (0–1 toward the next step), etaAt
(DateTime / unix s / ms) or eta('8 min') for fixed text, value($k, $v) /
values([...]), canceled(), done(), data([...]), title/subtitle/body
(override the template's texts). Start-only: lang('ar') forces one language for
everyone (default: each device's app language), alert($title, $body). Plain
methods: startTemplate($template, $id, $data, $options), updateData($id, $data),
end($id, ['data' => [...]]); a null inside $data clears a field. $template
may also be an inline design array. The start result includes iosTargets and,
when iOS got nothing, an iosWarning saying why.
Plain (your own iOS widget)
// Start — pass the activity type + id; ->toUser(..) for per-order, ->shared() for many. $nexus->liveActivities()->activity('DeliveryAttributes', 'order_42') ->title('Order #42')->status('Preparing')->progress(20) ->toUser('user_1')->priority(10) ->start(); // Update — activity type not needed; priority 5 = routine (unmetered), 10 = immediate. $nexus->liveActivities()->activity('order_42') ->status('On the way')->progress(70)->priority(5) ->update(); // End — optional final state + dismissal time. $nexus->liveActivities()->activity('order_42') ->status('Delivered')->dismissAt('2026-01-01T12:00:00Z') ->end();
Content-state helpers: title/subtitle/body/status/progress, plus set($k,$v) /
state([...]) for custom fields. Or call the plain methods
(start()/update()/end()) with arrays if you prefer.
iOS requires the APNs key configured in the console (Live Activities can't go through FCM) and the one-time Widget Extension in your app (see the Flutter SDK README). Android needs no extra setup.
18. Monolog handler
Forward your app's Monolog records to Nexus logs:
use Inverge\Nexus\Monolog\NexusLogHandler; $logger->pushHandler(new NexusLogHandler($nexus /*, level, bubble, flushAt */));
In Laravel, set NEXUS_LOGGING=true (and NEXUS_LOG_LEVEL) to wire this
automatically.
19. Low‑level request
For endpoints without a dedicated resource method:
$response = $nexus->request('POST', '/partner/events', ['events' => [...]], [ 'If-None-Match' => $etag, // extra headers ]);
request(string $method, string $path, ?array $json = null, array $headers = []): ?array
— returns the decoded body, or null on failure.