Search by

inverge / nexus

Inverge

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.

1.6.1 2026-10-21 21:42 UTC

This package is auto-updated.

Last update: 2026-10-21 21:43:57 UTC


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

  1. Installation
  2. Plain PHP setup
  3. Laravel setup
  4. Symfony setup
  5. Dispatchers (sync vs queued)
  6. Sessions
  7. Events
  8. Logs
  9. Errors
  10. Feature flags
  11. Remote Config
  12. Deep links & attribution
  13. Realtime
  14. Surveys
  15. Push notifications
  16. In‑app messages
  17. Live Activities
  18. Monolog handler
  19. 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. Return null from a to… 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.
  • NexusChannel is 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 in via() is left to that channel, so it's never sent twice.
  • Use the products' own channels when you want via() to pick per notifiable, or NotificationSent / NotificationFailed per 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): array
  • all(array $context = []): array
  • get(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.