Upgrading To v1.19 From v1.18
Exciting New Features 🎉
Enhancements 🚀
Breaking Changes 🛠
Upgrade Guide
As Golang v1.25 is no longer maintained, the Golang version Goravel supports has been upgraded from 1.25 to 1.26.
goravel/example project from v1.18 to v1.19 PR can be used as an upgrade reference: goravel/example#XXX.
You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically.
# Upgrade Guide for Goravel v1.19
**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.26 is installed, Goravel version ≥ 1.18.0.
## Step 1: Update Go module dependencies (Go ≥ 1.26 required)
**Detection**: Check which facade packages are already installed:
```shell
rg -l 'goravel/(gin|fiber|redis|s3|oss|cos|minio)' go.sum 2>/dev/null
```
**Action**: Run the framework upgrade command and upgrade only the facade packages detected in `go.sum`:
```shell
go get github.com/goravel/framework@latest
# For each facade found in detection, run the corresponding go get.
# If goravel/redis is in go.sum: go get github.com/goravel/redis@latest
# If goravel/gin is in go.sum: go get github.com/goravel/gin@latest
# If goravel/fiber is in go.sum: go get github.com/goravel/fiber@latest
# If goravel/s3 is in go.sum: go get github.com/goravel/s3@latest
# If goravel/oss is in go.sum: go get github.com/goravel/oss@latest
# If goravel/cos is in go.sum: go get github.com/goravel/cos@latest
# If goravel/minio is in go.sum: go get github.com/goravel/minio@latest
go mod tidy
```
## Step 2: Add retry_after to each queue connection
`retry_after` (default `60`, in seconds) is the crashed-worker reservation-expiry window: if a worker crashes while holding a job, its reservation expires after `retry_after` seconds and the job is recovered by other workers. Add it to each `database` and custom connection in `config/queue.go`:
```diff
"database": map[string]any{
"driver": "database",
"connection": "sqlite",
"queue": "default",
"concurrent": 5,
+ // Reservation expiry for crashed workers; must exceed the maximum job runtime
+ "retry_after": 60,
},
```
The value must exceed the maximum job runtime to avoid double-processing long-running jobs.
### Verification
```shell
rg -n 'retry_after' config/queue.go
go build ./...
```
## Step 3: Update custom queue drivers to the ReservedJob contract (only if you have custom drivers)
**Detection**: Check if any queue connection in `config/queue.go` uses a custom driver (a non-built-in `driver` value or a `"via"` key):
```shell
rg -n '"driver"\s*:\s*"custom"|"via"' config/queue.go
```
If no custom drivers are found, skip this step.
`contracts/queue.ReservedJob` now requires two new methods in addition to `Delete() error` and `Task() Task`:
- `Attempts() int` — the number of times the job has been attempted so far. It must be persisted with the reservation, so retry decisions survive worker restarts.
- `Release(delay time.Duration) error` — make the job available again after the given delay so it can be retried, incrementing attempts on the next pop.
```go
func (r *ReservedJob) Attempts() int {
return r.jobRecord.Attempts
}
// Release removes the job from the reserved set and makes it available
// again after the delay, preserving the serialized attempt count.
func (r *ReservedJob) Release(delay time.Duration) error {
// remove from reserved set and push to the delayed/ready set with score = now + delay
return nil
}
```
Also read the connection's `retry_after` config and use it to recover expired reservations left by crashed workers (see the [database driver](https://github.com/goravel/framework/blob/master/queue/driver_database.go) and the [Redis driver](https://github.com/goravel/redis/blob/master/queue.go) implementations).
### Verification
```shell
go build ./...
```
## Step 4: Bump the jobs table migration to millisecond-precision timestamps (optional)
For `database` queue connections, update the [20210101000002_create_jobs_table.go](https://github.com/goravel/goravel/blob/master/database/migrations/20210101000002_create_jobs_table.go) migration in `database/migrations` so new databases store `reserved_at`, `available_at`, and `created_at` with millisecond precision, so sub-second delays and retries survive round trips:
```diff
- table.DateTimeTz("reserved_at").Nullable()
- table.DateTimeTz("available_at")
- table.DateTimeTz("created_at").UseCurrent()
+ table.DateTimeTz("reserved_at", 3).Nullable()
+ table.DateTimeTz("available_at", 3)
+ table.DateTimeTz("created_at", 3).UseCurrent()
```
Existing databases do not need this migration unless you rely on sub-second retry delays.
## Step 5: Final verification
After completing all steps, run the full verification suite:
```shell
go build ./...
go vet ./...
go test ./... 2>&1 || true
```
If tests fail, the most common causes are:
- Custom queue drivers missing `Attempts()` or `Release()` on `ReservedJob` (step 3).Feature Introduction
Broadcasting
Goravel v1.19 introduces a first-party broadcasting module for pushing realtime, live-updating data to your frontend over WebSockets. Instead of the client polling the server for changes, your backend broadcasts events to named channels, and subscribed clients receive them instantly.
Install the facade with the package:install command:
./artisan package:install BroadcastThe initial release includes:
- Pusher-protocol broadcasting with
pusher,log, andnulldrivers — compatible with any Pusher protocol server such as Soketi. - Events implementing the
ShouldBroadcastcontract dispatched viafacades.Broadcast().Dispatch(), withBroadcastOnchannels,BroadcastAsevent names,BroadcastWithpayloads, andBroadcastWhenconditionals. - Public, private, and presence channels, authorization callbacks via
facades.Broadcast().Channel(), and themake:channelcommand to extract authorization into channel classes. - Receiving broadcasts on the frontend with Laravel Echo or any raw Pusher protocol WebSocket client.
err := facades.Broadcast().Dispatch(context.Background(), &events.OrderShipped{
OrderID: 1,
})Notifications
Goravel v1.19 introduces a first-party notification module for sending short, informational messages to your users across a variety of delivery channels (goravel/framework#1524). It ships with:
- Built-in
mailanddatabasechannels, plus support for custom channels through theChannelcontract andfacades.Notification().Extend(). - The
make:notificationcommand to scaffold notifications (with a--databaseflag for database-channel notifications) and thenotifications:tablecommand to generate thenotificationstable migration for the database channel. - Queued notifications through the
ShouldQueuecontract (OnQueue/OnConnection), withSendNow/NotifyNowfor synchronous delivery. - Optional per-notification contracts:
NotificationWithID,NotificationWithShouldSend,NotificationWithAfterSending,NotificationWithDatabaseConnection,NotificationWithTries, andNotificationWithBackoff, plus the notifiable-side typed routing contractsMailRoutable(RouteNotificationForMail) andDatabaseRoutable(RouteNotificationForDatabase).
Install the facade with the package:install command:
./artisan package:install Notificationerr := facades.Notification().Send(user, notifications.NewOrderShipped("12345"))The database channel stores notifications in the notifications table so they can be displayed in your web interface.
./artisan notifications:table
./artisan migrateRelease-based job retry with crash recovery
Jobs implementing ShouldRetry(err error, attempt, maxTries int) (retryable bool, delay time.Duration) are now released back to the queue on failure instead of being retried in-memory (goravel/framework#1531, goravel/redis#149). The attempt count is persisted with the reservation, so:
- Retries survive worker restarts and can be picked up by any worker.
- The release delay is respected across workers, with sub-second precision for the database and Redis drivers.
retryable = falselands the job in thefailed_jobstable.
// Retry while the attempt count is within the failure window, then give up.
func (r *TestRetryable) ShouldRetry(err error, attempt, maxTries int) (bool, time.Duration) {
if attempt <= 2 {
return true, 100 * time.Millisecond
}
return false, 0
}Queue connections also support a retry_after configuration option (default 60, in seconds) that controls the crashed-worker reservation-expiry window. If a worker crashes while holding a job, its reservation expires after retry_after seconds and the job is recovered by other workers. The value must exceed the maximum job runtime to avoid double-processing long-running jobs:
"database": map[string]any{
"driver": "database",
"connection": "sqlite",
"queue": "default",
"concurrent": 5,
// Reservation expiry for crashed workers; must exceed the maximum job runtime
"retry_after": 60,
},The jobs table migration now stores reserved_at, available_at, and created_at with millisecond precision, so sub-second delays and release-based retries survive round trips to the database:
table.DateTimeTz("reserved_at", 3).Nullable()
table.DateTimeTz("available_at", 3)
table.DateTimeTz("created_at", 3).UseCurrent()New installations get this automatically. Existing databases can keep their second-precision columns unless you rely on sub-second retry delays.
ReservedJob requires Attempts and Release
contracts/queue.ReservedJob now requires two new methods in addition to Delete() error and Task() Task:
type ReservedJob interface {
// Attempts returns the number of times the job has been attempted so far.
Attempts() int
// Delete removes the job from the queue.
Delete() error
// Release makes the job available again after the given delay so it can
// be retried, incrementing attempts on the next pop.
Release(delay time.Duration) error
// Task returns the task to execute.
Task() Task
}Custom queue drivers must be updated to implement Attempts() and Release(). See the Upgrade Guide above for the full migration steps.
