Skip to content

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.

markdown
# 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:

shell
./artisan package:install Broadcast

The initial release includes:

  • Pusher-protocol broadcasting with pusher, log, and null drivers — compatible with any Pusher protocol server such as Soketi.
  • Events implementing the ShouldBroadcast contract dispatched via facades.Broadcast().Dispatch(), with BroadcastOn channels, BroadcastAs event names, BroadcastWith payloads, and BroadcastWhen conditionals.
  • Public, private, and presence channels, authorization callbacks via facades.Broadcast().Channel(), and the make:channel command to extract authorization into channel classes.
  • Receiving broadcasts on the frontend with Laravel Echo or any raw Pusher protocol WebSocket client.
go
err := facades.Broadcast().Dispatch(context.Background(), &events.OrderShipped{
    OrderID: 1,
})

View Document

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 mail and database channels, plus support for custom channels through the Channel contract and facades.Notification().Extend().
  • The make:notification command to scaffold notifications (with a --database flag for database-channel notifications) and the notifications:table command to generate the notifications table migration for the database channel.
  • Queued notifications through the ShouldQueue contract (OnQueue / OnConnection), with SendNow / NotifyNow for synchronous delivery.
  • Optional per-notification contracts: NotificationWithID, NotificationWithShouldSend, NotificationWithAfterSending, NotificationWithDatabaseConnection, NotificationWithTries, and NotificationWithBackoff, plus the notifiable-side typed routing contracts MailRoutable (RouteNotificationForMail) and DatabaseRoutable (RouteNotificationForDatabase).

Install the facade with the package:install command:

shell
./artisan package:install Notification
go
err := 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.

shell
./artisan notifications:table
./artisan migrate

View Document

Release-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 = false lands the job in the failed_jobs table.
go
// 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:

go
"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:

go
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.

View Document

ReservedJob requires Attempts and Release

contracts/queue.ReservedJob now requires two new methods in addition to Delete() error and Task() Task:

go
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.

View Document

Released under the MIT License