Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pushok Laravel Package

edamov/pushok

Pushok is a PHP library for sending iOS push notifications via Apple's APNs HTTP/2 API. It supports JWT token and certificate authentication, custom payloads, and batch delivery, making it easy to add reliable Apple Push Notification service support to Laravel or any PHP app.

Deep Wiki
Context7

Getting Started

Install with Composer (PHP 7.1+ with an HTTP/2–capable libcurl build):

composer require edamov/pushok

Pushok is a thin, object-oriented wrapper around APNs HTTP/2, so there are only four classes you need to internalize:

Class Responsibility
Pushok\AuthProvider\Token / Certificate Credentials (.p8 JWT key or .pem cert)
Pushok\Client HTTP/2 transport, batching, returns Response[]
Pushok\Payload + Pushok\Payload\Alert The aps dictionary and custom data
Pushok\Notification Glue object: one payload + one device token

Minimal working send (token auth, sandbox):

use Pushok\AuthProvider\Token;
use Pushok\Client;
use Pushok\Notification;
use Pushok\Payload;
use Pushok\Payload\Alert;

$auth = Token::create([
    'key_id'       => config('pushok.key_id'),          // 10-char Key ID from Apple
    'team_id'      => config('pushok.team_id'),
    'private_key'  => storage_path('app/AuthKey_ABC123.p8'), // PATH, not contents
    'passphrase'   => null,
]);

$client = new Client($auth, false); // false = sandbox, true = production

$alert = Alert::create()
    ->setTitle('Order shipped')
    ->setBody('Your parcel is on its way.');

$payload = Payload::create()
    ->setAlert($alert)
    ->setBadge(1)
    ->setSound('default')
    ->setPushType(Payload::PUSH_TYPE_ALERT)
    ->setTopic('com.example.app');

$client->addNotification(new Notification($payload, $deviceToken));

foreach ($client->push() as $response) {
    if ($response->getStatusCode() !== 200) {
        logger()->warning('APNs rejected', [
            'apns_id' => $response->getApnsId(),
            'status'  => $response->getStatusCode(),
            'reason'  => $response->getErrorReason(),
        ]);
    }
}

First use case to implement: an "account activity" alert (title, body, badge, sound) sent to a token you captured at login. Validate it against the sandbox endpoint using a development build — it's the fastest way to confirm your key, team ID, topic, and environment flag are all correct before you wire anything into a queue.

Look first at the constructor signatures of Client and AuthProvider\Token, then at Payload's setters — that's 90% of the day-to-day API surface.

Implementation Patterns

1. Bind a singleton client in a service provider

Client holds a credential and connection state; build it once, resolve it from the container, keep config in config/services.php.

// config/services.php
'pushok' => [
    'key_id'      => env('APNS_KEY_ID'),
    'team_id'     => env('APNS_TEAM_ID'),
    'private_key' => env('APNS_PRIVATE_KEY_PATH'),
    'production'  => env('APNS_PRODUCTION', false),
    'topic'       => env('APNS_TOPIC', 'com.example.app'),
],
// AppServiceProvider::register()
$this->app->singleton(\Pushok\Client::class, function () {
    $auth = \Pushok\AuthProvider\Token::create(config('services.pushok'));
    $auth = tap($auth, fn ($a) => $a->setIsProduction(config('services.pushok.production')));

    return new \Pushok\Client($auth, (bool) config('services.pushok.production'));
});

Wrap this behind your own ApnsGateway interface so tests, Horizon jobs, and controllers never touch Pushok directly — that interface is also your hook for swapping in a fake, which matters because Pushok ships no test transport.

2. A queued job per batch, not per notification

Client::push() opens a single HTTP/2 connection and dispatches all queued notifications concurrently. That makes fan-out cheap: chunk tokens (500–1000 per batch), queue one job per chunk, and get one Response per notification back.

class SendPushBatch implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $backoff = 5;

    public function __construct(public array $tokens, public array $payloadData) {}

    public function handle(ApnsGateway $apns): void
    {
        $responses = $apns->sendMany($this->tokens, $this->payloadData);

        foreach ($responses as $token => $response) {
            match (true) {
                $response->getStatusCode() === 410 => DeviceToken::where('token', $token)->delete(),
                in_array($response->getErrorReason(), ['BadDeviceToken', 'DeviceTokenNotForTopic'])
                    => DeviceToken::where('token', $token)->update(['invalid' => true]),
                default => null,
            };
        }
    }
}

Key the responses back to tokens yourself (Pushok returns a numerically indexed array, not keyed by device token).

3. Silent / background notifications

For sync triggers, omit the alert entirely and set push type + priority. Background pushes must be priority 5 and cannot carry an alert.

$payload = Payload::create()
    ->setContentAvailable(true)
    ->setPushType('background')
    ->setPriority(5)
    ->setTopic(config('services.pushok.topic'))
    ->setCustomProperty('sync', ['entity' => 'invoices', 'since' => now()->timestamp]);

4. VoIP (PushKit) requires a distinct topic

APNs will return TopicDisallowed otherwise. Use the bundle ID plus .voip, push type voip, and put your call data at the root of the payload.

$payload = Payload::create()
    ->setPushType('voip')
    ->setTopic(config('services.pushok.topic').'.voip')
    ->setCustomProperty('handle', $callerId)
    ->setCustomProperty('uuid', (string) Str::uuid());

VoIP payloads are allowed up to 5KB; everything else is 4KB.

5. Localized alerts

Build the alert with loc keys rather than pre-rendering strings — APNs does the localization on-device and you avoid leaking user locale into the payload:

$alert = Alert::create()
    ->setTitleLocKey('order.shipped.title')
    ->setTitleLocArgs(['#1234'])
    ->setLocKey('order.shipped.body')
    ->setLocArgs(['2026-05-13'])
    ->setSound('default');

6. Model it as a Laravel notification channel (optional but nice)

If pushes are one of several delivery channels in your app, wrap Pushok in a channel so Notification::send() fans out consistently:

public function send($notifiable, Notification $notification): void
{
    $message = $notification->toApns($notifiable); // returns your DTO
    app(ApnsGateway::class)->send($notifiable->routeNotificationForApns(), $message);
}

7. Token lifecycle

Store device tokens in their own table with user_id, token, environment (sandbox/production), last_seen_at, and invalid_at. Tokens are per-install, not per-user, so plan for a user having several. Sweep on login and prune on 410 Unregistered.

Gotchas and Tips

  • private_key is a filesystem path, not the key body. Passing file_get_contents() of the .p8 throws an invalid-key exception. If you source secrets from Vault/KMS, write the key to a temp file (and delete it) or implement the Pushok\AuthProvider contract yourself.
  • HTTP/2 must be compiled into libcurl. Alpine and some slim images ship curl without it, which surfaces as a generic connection failure, not an APNs error. Sanity-check: curl_version()['features'] & CURL_VERSION_HTTP2.
  • BadDeviceToken usually means the wrong environment, not a bad token. A token from a TestFlight/App Store build will never work against false/sandbox. Tag every token row with the environment the app reported and pick the client accordingly.
  • APNs rejections are responses, not exceptions. push() returns a Response per notification with getStatusCode() and getErrorReason(). Only pre-flight problems (missing key, unreadable cert, malformed payload) throw. Wrapping push() in try/catch alone is a silent-failure trap — always iterate the responses.
  • getStatusCode() === 200 with an empty body is success. Don't parse the body on 200; read getApnsId() for correlation and log it — it's what Apple support will ask for.
  • Set apns-topic and apns-push-type explicitly. Since iOS 13 both are effectively mandatory; missing push type yields MissingTopic/BadPushType, and a topic that doesn't match the signing provision yields TopicDisallowed. Don't rely on defaults.
  • 4KB limit (5KB for VoIP). PayloadTooLarge / 413 is easy to hit once you attach custom data. Measure strlen(json_encode($payload)) in a test and enforce a cap in your DTO before it reaches the queue.
  • The Client is stateful. Notifications accumulate until push(). If you reuse one instance across the request or across queue iterations, verify your pinned version clears the queue after pushing — otherwise you'll silently re-send. Safest pattern: resolve the singleton per job and add only that job's notifications.
  • JWT lifetimes matter for long-running workers. APNs rejects provider tokens older than one hour and throttles regeneration more than once every ~20 minutes (TooManyProviderTokenUpdates). Long-lived queue workers and Octane processes should be recycled (or re-resolve the auth provider) rather than holding a token for days.
  • Handle 429 and 503 with backoff, not retries-in-place. TooManyRequests and ServiceUnavailable need exponential backoff; 500/503 may not include an apns-id, so don't treat its absence as a parsing bug.
  • Payload::setCustomProperty() cannot overwrite aps. Everything you pass goes to the payload root — good for fcm_options, content-available-adjacent data, VoIP handle, but collisions with reserved keys are ignored or throw depending on version.
  • Certificate auth needs a combined PEM (cert + private key, optionally with a passphrase). Token auth is strictly better for CI/CD and rotation — prefer it unless you're stuck with a legacy cert.
  • Extension points: the AuthProvider abstraction (swap credential sources) and your own gateway wrapper (swap transport, add metrics, fake in tests). Pushok deliberately doesn't ship a fake client, so build FakeApnsGateway early — asserting on constructed payloads is far cheaper than integration-testing against Apple.
  • Debugging checklist when nothing arrives on device: response 200 → wrong environment → wrong topic → app not registered for the push type (e.g. missing PushKit entitlement for VoIP) → silent push throttled because you set content-available without priority 5.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib