badawy24 / pushify
Laravel multi-provider push notification package with Firebase and OneSignal support.
Requires
- php: ^8.2
- illuminate/cache: ^12.0 || ^13.0
- illuminate/console: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/routing: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
README
Laravel backend package for sending push notifications through one selected provider at a time, with OneSignal device subscription management built in.
Supported providers:
| Provider | Scheduling | Subscriptions |
|---|---|---|
| ๐ฅ Firebase Cloud Messaging (FCM) | Via scheduled command | โ |
| ๐ฃ OneSignal | Native send_after |
โ Device register & logout |
Project Structure
pushify/
โโโ config/
โ โโโ pushify.php
โโโ database/
โ โโโ migrations/
โ โโโ 2026_01_01_000000_create_pushify_notifications_table.php
โ โโโ 2026_01_02_000000_create_pushify_subscriptions_table.php
โ โโโ 2026_01_03_000000_add_pushify_external_id_to_users_table.php
โโโ routes/
โ โโโ pushify.php
โโโ src/
โ โโโ Commands/
โ โ โโโ SendScheduledPushifyNotifications.php
โ โโโ Concerns/
โ โ โโโ HasPushifyExternalId.php
โ โโโ Contracts/
โ โ โโโ PushifyProviderInterface.php
โ โ โโโ PushifyServiceInterface.php
โ โ โโโ PushifySubscriptionsInterface.php
โ โโโ Factories/
โ โ โโโ PushifyProviderFactory.php
โ โโโ Http/
โ โ โโโ Controllers/
โ โ โ โโโ PushifyController.php
โ โ โโโ Requests/
โ โ โ โโโ StorePushifyRequest.php
โ โ โโโ Resources/
โ โ โโโ PushifyResource.php
โ โโโ Models/
โ โ โโโ Pushify.php
โ โ โโโ PushifySubscription.php
โ โโโ Providers/
โ โ โโโ FirebaseProvider.php
โ โ โโโ OneSignalProvider.php
โ โโโ Services/
โ โ โโโ FirebaseService.php
โ โ โโโ OneSignalService.php
โ โ โโโ PushifyService.php
โ โ โโโ PushifySubscriptionsService.php
โ โโโ Support/
โ โ โโโ PushifyExternalIdGenerator.php
โ โโโ PushifyServiceProvider.php
โโโ stubs/
โ โโโ Http/
โ โ โโโ Controllers/
โ โ โ โโโ PushifyController.stub
โ โ โโโ Requests/
โ โ โ โโโ StorePushifyRequest.stub
โ โ โโโ Resources/
โ โ โโโ PushifyResource.stub
โ โโโ routes/
โ โโโ pushify.stub
โโโ composer.json
Table of Contents
- Requirements
- Installation
- Publish
- Migration
- Configuration
- Routes
- Usage
- Notification Statuses
- Scheduled Command
- Customizing the HTTP Layer
- Adding a Custom Provider
- Store Endpoint Payload
- Response Structure
- Authors
- License
Requirements
- PHP >= 8.2
- Laravel >= 12.0
- OpenSSL PHP extension (required for Firebase JWT signing โ no external Google SDK needed)
- A
userstable (required before running Pushify migrations)
Installation
composer require badawy24/pushify
Then clear the cache:
php artisan optimize:clear
Publish
Publish everything at once
php artisan vendor:publish --tag=pushify
This publishes only the files you are expected to edit:
config/pushify.php
routes/pushify.php
app/Http/Controllers/Pushify/PushifyController.php
app/Http/Requests/Pushify/StorePushifyRequest.php
app/Http/Resources/Pushify/PushifyResource.php
Core services, providers, factories, models, commands, and contracts stay inside the package and are never published.
Publish separately
# Config only php artisan vendor:publish --tag=pushify-config # Routes only php artisan vendor:publish --tag=pushify-routes # Controller, Request, Resource php artisan vendor:publish --tag=pushify-http
Migration
Important: Your
userstable must exist before running migrations. If it does not, the migration will abort with a clear error message.
php artisan migrate
Tables created / modified
pushify_notifications โ notification log:
id, title, body, image, data, scheduled_at,
status, sent_at, failed_at, error_message,
created_at, updated_at
pushify_subscriptions โ registered devices:
id, external_id, device_token, subscription_id,
device_type, created_at, updated_at
users โ column added:
pushify_external_id (nullable, unique) e.g. AGFGFFGY_1
The pushify_external_id is generated automatically on first use: 8 random chars + _ + user_id.
Configuration
Published config file: config/pushify.php
Only one provider is active at a time, selected from .env.
๐ฅ Firebase
PUSHIFY_PROVIDER=firebase FIREBASE_CREDENTIALS=storage/firebase/firebase.json
Place your Firebase service account JSON file at storage/firebase/firebase.json.
Firebase sends to a topic โ default is all. Configure it in the published config:
'firebase' => [ 'topic' => 'all', ],
Your mobile app must subscribe devices to the same topic.
๐ฃ OneSignal
PUSHIFY_PROVIDER=onesignal ONESIGNAL_APP_ID=your-app-id ONESIGNAL_API_KEY=your-api-key ONESIGNAL_API_URL=https://api.onesignal.com/notifications
OneSignal sendToAll sends to included_segments => ['All'].
Users (external ID)
PUSHIFY_USERS_TABLE=users PUSHIFY_EXTERNAL_ID_COLUMN=pushify_external_id
Optional
# Log full request payload โ disable in production PUSHIFY_LOG_PAYLOAD=false # Disable package routes if you prefer to define your own PUSHIFY_ROUTES_ENABLED=true # Change the route prefix (default: pushify) PUSHIFY_ROUTE_PREFIX=pushify
Routes
The package registers these routes automatically:
GET /pushify List all notifications (paginated)
POST /pushify Create and send
GET /pushify/{pushify} Show one
POST /pushify/{pushify}/send Send an existing notification
Check registered routes:
php artisan route:list | grep pushify
Adding Auth Middleware
After publishing, open routes/pushify.php and update the middleware:
Route::prefix(config('pushify.routes.prefix', 'pushify')) ->middleware(['api', 'auth:sanctum']) ->group(function () { // routes... });
Usage
The package exposes two separate interfaces:
| Interface | Purpose |
|---|---|
PushifyServiceInterface |
Send notifications |
PushifySubscriptionsInterface |
Register & remove devices (OneSignal only) |
Both are bound automatically โ no manual binding required.
Notifications
Inject the service
use Badawy\Pushify\Contracts\PushifyServiceInterface;
Send to all
$notification = $push->sendToAll( title: 'New offer', body: 'Check our latest offers now', data: [ 'type' => 'offer', 'offer_id' => 15, ], image: 'https://example.com/image.jpg', scheduledAt: null, );
Send to a specific user
Use sendToUserById() with your local user_id โ the package resolves pushify_external_id from the users table automatically:
$notification = $push->sendToUserById( userIds: $user->id, title: 'Order updated', body: 'Your order status has changed', data: [ 'type' => 'order_status_updated', 'order_id' => 15, ], );
OneSignal delivers to all devices registered under that user's external_id.
You can also pass external_id strings directly via sendToUser():
$notification = $push->sendToUser( userIds: [$user->pushifyExternalId()], title: 'Order updated', body: 'Your order status has changed', data: ['type' => 'order_status_updated'], );
Schedule a notification
$notification = $push->sendToAll( title: 'Upcoming sale', body: 'Our sale starts in one hour', data: ['type' => 'sale'], image: null, scheduledAt: now()->addHour()->toDateTimeString(), );
| Provider | Behavior |
|---|---|
| ๐ฅ Firebase | Saved as pending โ sent by the command when scheduled_at <= now() |
| ๐ฃ OneSignal | Submitted immediately with native send_after โ OneSignal handles the delay |
Create only (without sending)
$notification = $push->create([ 'title' => 'Draft notification', 'body' => 'Notification body', 'data' => ['type' => 'general'], 'image' => null, 'scheduled_at' => null, ]);
Send an existing notification
use Badawy\Pushify\Models\Pushify; $notification = Pushify::findOrFail($id); $notification = $push->send($notification);
PushifyServiceInterface methods
public function create(array $payload): Pushify; public function sendToAll( string $title, string $body, array $data = [], ?string $image = null, ?string $scheduledAt = null ): Pushify; public function sendToUser( array|string $userIds, string $title, string $body, array $data = [], ?string $image = null, ?string $scheduledAt = null ): Pushify; public function sendToUserById( array|int $userIds, string $title, string $body, array $data = [], ?string $image = null, ?string $scheduledAt = null ): Pushify; public function send(Pushify $notification): Pushify; public function markScheduledAsSent(): int;
Subscriptions (OneSignal)
Requires
PUSHIFY_PROVIDER=onesignal.
Setup โ add trait to your User model
use Badawy\Pushify\Concerns\HasPushifyExternalId; class User extends Authenticatable { use HasPushifyExternalId; }
Inject the service
use Badawy\Pushify\Contracts\PushifySubscriptionsInterface;
Full flow
class AuthController extends Controller { public function __construct( private readonly PushifySubscriptionsInterface $subscriptions, private readonly PushifyServiceInterface $pushify, ) {} // 1. Login โ register device public function registerDevice(Request $request) { $subscription = $this->subscriptions->subscribe( userId: $request->user()->id, token: $request->input('device_token'), data: ['type' => 'Android'], ); return response()->json($subscription); } // 2. Send notification (from anywhere in your app) public function notifyUser(int $userId) { $this->pushify->sendToUserById( userIds: $userId, title: 'Hello', body: 'You have a new notification', data: ['type' => 'general'], ); } // 3. Logout โ remove device public function logout(Request $request) { $this->subscriptions->unsubscribe($request->input('device_token')); // ... your logout logic } }
What happens internally
| Step | Method | Action |
|---|---|---|
| Login | subscribe($userId, $token) |
Generates pushify_external_id on users, registers device on OneSignal, saves device_token + subscription_id in pushify_subscriptions |
| Notify | sendToUserById($userId, ...) |
Resolves pushify_external_id from users, sends via OneSignal to all user devices |
| Logout | unsubscribe($deviceToken) |
Deletes subscription from OneSignal, removes row from pushify_subscriptions |
PushifySubscriptionsInterface methods
public function subscribe(int $userId, string $token, array $data = []): PushifySubscription; public function unsubscribe(string $deviceToken): void;
Optional $data fields for subscribe
| Field | Description |
|---|---|
type |
Device type, e.g. Android, iOS |
language |
User language, e.g. ar |
timezone_id |
e.g. Africa/Cairo |
country |
e.g. EG |
tags |
Key-value tags array |
device_model |
e.g. iPhone 15 |
device_os |
e.g. iOS 18 |
app_version |
e.g. 1.0.0 |
Quick test via Tinker
php artisan tinker
// Register device app(\Badawy\Pushify\Contracts\PushifySubscriptionsInterface::class) ->subscribe(1, 'your-fcm-token', ['type' => 'Android']); // Send notification app(\Badawy\Pushify\Contracts\PushifyServiceInterface::class) ->sendToUserById(1, 'Hello', 'Test from Tinker', ['type' => 'test']); // Logout device app(\Badawy\Pushify\Contracts\PushifySubscriptionsInterface::class) ->unsubscribe('your-fcm-token');
Notification Statuses
| Status | Meaning |
|---|---|
pending |
Stored, not sent yet |
processing |
Currently being dispatched to the provider |
scheduled |
Submitted to OneSignal with a future send_after |
sent |
Successfully delivered to the provider |
failed |
Failed โ see error_message column in the database |
Scheduled Command
php artisan pushify:send-scheduled
| Provider | What it does |
|---|---|
| ๐ฅ Firebase | Sends all pending notifications where scheduled_at <= now() |
| ๐ฃ OneSignal | Marks all scheduled notifications where scheduled_at <= now() as sent locally โ OneSignal already delivered them |
Add to Laravel Scheduler
In routes/console.php:
Schedule::command('pushify:send-scheduled')->everyMinute();
Or via cron:
* * * * * php /path/to/project/artisan pushify:send-scheduled >> /dev/null 2>&1
Customizing the HTTP Layer
After publishing with --tag=pushify-http, you can freely edit:
| File | Purpose |
|---|---|
app/Http/Controllers/Pushify/PushifyController.php |
Request handling & response |
app/Http/Requests/Pushify/StorePushifyRequest.php |
Validation rules & authorization |
app/Http/Resources/Pushify/PushifyResource.php |
JSON output shape |
The published controller injects PushifyServiceInterface โ extend or replace any logic without touching the package internals.
Adding a Custom Provider
Step 1 โ Create your provider class:
namespace App\Pushify\Providers; use Badawy\Pushify\Contracts\PushifyProviderInterface; class CustomProvider implements PushifyProviderInterface { public function sendToAll( string $title, string $body, array $data = [], ?string $image = null, ?string $scheduledAt = null ): array { return ['success' => true]; } public function sendToUser( array|string $userIds, string $title, string $body, array $data = [], ?string $image = null, ?string $scheduledAt = null ): array { return ['success' => true]; } }
Step 2 โ Register it in config/pushify.php:
'providers' => [ 'firebase' => \Badawy\Pushify\Providers\FirebaseProvider::class, 'onesignal' => \Badawy\Pushify\Providers\OneSignalProvider::class, 'custom' => \App\Pushify\Providers\CustomProvider::class, ],
Step 3 โ Activate it in .env:
PUSHIFY_PROVIDER=custom
Store Endpoint Payload
POST /pushify
{
"title": "New offer",
"body": "Check our latest offers",
"image": "https://example.com/image.jpg",
"data": {
"type": "offer",
"offer_id": 15
},
"scheduled_at": null
}
Scheduled example:
{
"title": "Scheduled offer",
"body": "This will be sent later",
"image": null,
"data": { "type": "offer" },
"scheduled_at": "2026-06-01 09:00:00"
}
Response Structure
All endpoints return a consistent JSON envelope:
{
"data": {
"id": 1,
"title": "New offer",
"body": "Check our latest offers",
"image": "https://example.com/image.jpg",
"data": { "type": "offer", "offer_id": "15" },
"scheduled_at": null,
"status": "sent",
"sent_at": "2026-01-01T12:00:00+00:00",
"failed_at": null,
"created_at": "2026-01-01T11:59:00+00:00",
"updated_at": "2026-01-01T12:00:00+00:00"
}
}
Authors
License
MIT