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

Apn Laravel Package

laravel-notification-channels/apn

Send push notifications to iOS, macOS, and watchOS from Laravel using Apple's APN service. This notification channel supports token and certificate authentication, HTTP/2, custom payloads, and easy integration with Laravel's notification system.

View on GitHub
Deep Wiki
Context7

Getting Started

Day one is configuration, not code. The channel is thin — almost all of the work you'll do is in setup and in what you put into toApn().

composer require laravel-notification-channels/apn
php artisan vendor:publish --provider="NotificationChannels\Apn\ApnServiceProvider"

Populate config/apn.php and .env with the three things from the Apple Developer portal (Certificates, Identifiers & Profiles → Keys): Key ID, Team ID, and the .p8 file. Apple only lets you download the .p8 once — drop it in storage/app/apn/AuthKey_ABC123.p8 (outside the webroot) and reference the path in env, never the contents. Add your bundle ID (this becomes the apns-topic) and the sandbox/production flag.

Two pre-flight checks before writing product code:

  1. Your cache store is shared. Since v6.4.0 the provider token is cached for 20 minutes. File cache on a single node is correct but forfeits token reuse across processes — which is the whole point at volume.
  2. You know your Laravel/PHP versions. The token-cache rework exists partly for Laravel 13's stricter cache controls; pin ^6.4 and don't assume older majors behave the same.

Where to look first: config/apn.php, then the ApnMessage class in vendor/laravel-notification-channels/apn/src/. Those public methods are the payload API — the README example is representative, not exhaustive.

First use case — a transactional alert. Add the channel to via(), a route method to the notifiable, and a toApn():

// app/Models/User.php
public function routeNotificationForApn()
{
    return $this->device_token; // nullable
}

// app/Notifications/OrderShipped.php
public function via($notifiable)
{
    return [ApnChannel::class];
}

public function toApn($notifiable)
{
    return ApnMessage::create()
        ->title('Order shipped')
        ->body("Order #{$this->order->id} is on its way.")
        ->badge(1)
        ->sound('default')
        ->setId((string) Str::uuid()); // apns-id for log correlation
}

Smoke-test from tinker without touching a real notifiable model, using an on-demand notification:

Notification::route('apn', $deviceToken)->notify(new OrderShipped($order));

If that returns without throwing, credentials, topic, and endpoint are correct. Do this before building device tables, fan-out jobs, or payload abstractions — it's the cheapest possible spike.

Implementation Patterns

Route to a device table, not a column. users.device_token survives about a sprint. Real apps have iPhone + iPad + reinstalls, and you need a row you can delete when APNs says Unregistered.

public function routeNotificationForApn()
{
    return $this->deviceTokens()->pluck('token');
}

Verify fan-out against your installed version; if it only accepts a single token, loop inside a queued job — but still keep the table.

Queue by default. Push is an outbound HTTP/2 call to a third party. Implement ShouldQueue (or $user->notify() inside a job), give push its own queue so a slow APNs response can't starve mail or webhooks, and keep the worker --timeout below retry_after. This is also where the 20-minute token cache pays off: a 10k-token fan-out shares one minted provider token instead of hammering Apple's auth endpoint.

Keep toApn() as the single payload surface. The channel is an implementation detail; express intent as a notification class. If you have several, put shared shape (badge policy, sound, category constants) in a base PushNotification or a small ApnMessage factory. Vendor classes should not leak into controllers or services — that keeps a future package swap a one-file change.

Categories and custom keys for actionable + deep-linked pushes.

ApnMessage::create()
    ->title('New message')
    ->category('MESSAGE')            // must match UNNotificationCategory in iOS code
    ->custom('conversation_id', $id) // structured, not stringified into body
    ->contentAvailable(true);        // silent/data-only when there's no alert

Treat silent pushes as best-effort — iOS budgets background pushes and drops them under low-power conditions. Never make them the only mechanism for state the app depends on.

Testing is two-layered. Notification::fake() intercepts at the notification layer:

Notification::fake();
$user->notify(new OrderShipped($order));

Notification::assertSentTo($user, OrderShipped::class, function ($notification, $channels) {
    return in_array(ApnChannel::class, $channels);
});

Then unit-test toApn() in isolation by asserting on the returned ApnMessage's public accessors. Keep one integration path — staging device or a manual checklist — that actually hits the sandbox endpoint. Fakes will happily validate a payload Apple rejects.

Handle token failures in the job, not by ignoring errors. Catch NotificationChannels\Apn\Exceptions\CouldNotSendNotification in failed(): on Unregistered / BadDeviceToken, soft-delete the token row and mark the job successful — retrying will never succeed. Retry with backoff on 429, 503, and TooManyProviderTokenUpdates.

Multiple apps or environments. Config is global. A family of iOS apps means either separate deployments or building the client with per-app credentials and injecting it rather than resolving the container singleton. Decide early — retrofitting per-tenant credentials after the cache is in play is how 403 InvalidProviderToken bugs are born.

Gotchas and Tips

  • Token cache keys are your highest-severity risk. v6.4.0 caches the provider token (not the whole client) for 20 minutes. If the key doesn't isolate by environment, Team/Key ID, and bundle/topic, a token minted for one app gets reused for another — and it fails intermittently, only within the TTL window. Verify the key composition in the source, and namespace your cache store per app as belt-and-braces.
  • Credential rotation has a 20-minute stale window. Rotating the .p8 or Key ID needs a token-cache flush in the same runbook step, or the revoked key keeps working until TTL expiry and you burn an hour debugging "revoked but functional."
  • Upgrading from pre-6.4? Older releases cached the entire APN client object. Bump your cache namespace or flush on deploy, and delete any application code that cached the client yourself — otherwise you're running the old anti-pattern next to the new behavior. Bonus: the new design keeps credential-bearing objects out of your cache backend, worth citing in a security sign-off.
  • A mangled .p8 is the most common "it just doesn't work." It must retain -----BEGIN PRIVATE KEY----- / -----END PRIVATE KEY----- and a trailing newline. Inline env vars eat newlines; store a path. Symptom: 403 on every send, no useful error.
  • Don't new the client per send. Resolve from the container so the token cache is actually shared. Constructing one inside a loop re-mints tokens — exactly how teams earn 429 TooManyProviderTokenUpdates.
  • config:cache is a real footgun. Only env() calls inside config/apn.php survive the cache. Calling env('APN_KEY_ID') from a service provider works locally and returns null in production.
  • Sandbox tokens are not production tokens. A debug-build token sent to production returns BadDeviceToken. Bind APN_PRODUCTION to your environment; never copy a dev token into a staging fixture.
  • 4KB payload ceiling. Long bodies plus fat custom payloads fail as PayloadTooLarge. Truncate server-side and send IDs, not serialized objects.
  • "Sent successfully" ≠ "shown." Permissions and Focus mode intervene. Don't build logic that assumes delivery; the apns-id from setId() is for log correlation, not proof of display.
  • Simulator testing: xcrun simctl push <device> <bundle-id> payload.apns with a hand-written payload for client work; real device for end-to-end. Simulators can't use your APNs key.
  • Dependency posture: 201 stars, zero dependents, effectively one maintainer. Pin ^6.4, keep the channel behind your own notification abstraction, and know your exit plan. It sends pushes — it does not segment, orchestrate, or report; resist that scope creep.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console