Skip to content

Upgrading To v1.18 From v1.17

Exciting New Features 🎉

Enhancements 🚀

Breaking Changes 🛠

Upgrade Guide

As Golang v1.24 is no longer maintained, the Golang version of Goravel supports has been upgraded from 1.24 to 1.25.

goravel/example project from v1.17 to v1.18 PR can be used as an upgrade reference: goravel/example#133.

You can copy and paste the following into your AI coding agent to upgrade your Goravel project automatically.

markdown
# Upgrade Guide for Goravel v1.18

**Before you begin**: ensure all changes are committed or stashed, the project builds cleanly (`go build ./...`), and Go ≥ 1.25 is installed, Goravel version ≥ 1.17.0.

## Step 1: Update Go module dependencies

**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/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/redis is in go.sum: go get github.com/goravel/redis@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: Migrate HTTP middleware from func to interface

`contracts/http.Middleware` changed from `func(http.Context)` to an interface with `Handle(http.Context)` and `Signature() string` methods. Every custom middleware must be updated.

### 2a. Detect all custom middleware functions

```shell
rg -n 'func\s+\w+\s*\(.*\)\s+http\.Middleware' --type go
rg -n 'func\s+\w+\s*\(.*\)\s+contractshttp\.Middleware' --type go
```

Also search for anonymous middleware (inline `return func(ctx http.Context)`):

```shell
rg -n 'return\s+func\(ctx\s+http\.Context\)' --type go
```

### 2b. Convert each middleware

For each detected middleware, keep the factory function signature and replace the closure body with a struct literal:

```diff
 func Auth() http.Middleware {
-    return func(ctx http.Context) {
-        ctx.Request().Next()
-    }
+    return &Auth{}
 }

+type Auth struct{}
+
+func (a *Auth) Handle(ctx http.Context) {
+    ctx.Request().Next()
+}
+
+func (a *Auth) Signature() string {
+    return "auth"
+}
```

If the original factory accepted parameters, pass them to the struct literal:

```diff
 func Throttle(name string) http.Middleware {
-    return func(ctx http.Context) {
-        // throttle logic using name
-    }
+    return &Throttle{Name: name}
 }
+
+type Throttle struct{ Name string }
+
+func (t *Throttle) Handle(ctx http.Context) { /* throttle logic using t.Name */ }
+func (t *Throttle) Signature() string { return "throttle-" + t.Name }
```

Call sites like `middleware.Auth()` stay unchanged — the factory function still returns `http.Middleware`, but now returns a struct instead of a closure.

### 2c. Verification

```shell
# Ensure no remaining func-returning middleware patterns
rg -n 'func.*http\.Middleware' --type go

# Build must succeed
go build ./...
```

## Step 3: Update validation rule and filter maps

Validation rules and filters now use `map[string]any` instead of `map[string]string`.

### 3a. Detect affected files

```shell
rg -n 'map\[string\]string.*Validate\(' --type go
rg -n 'map\[string\]string.*Make\(' --type go
rg -n 'Rules\(\).*map\[string\]string' --type go
rg -n 'Filters\(\).*map\[string\]string' --type go
rg -n 'Messages\(\).*map\[string\]string' --type go
rg -n 'Attributes\(\).*map\[string\]string' --type go
```

Also search for `Validate` and `Make` calls that may already use `map[string]any` or use short variable declarations:

```shell
rg -n '\.Validate\(.*map\[string\]' --type go
rg -n 'validation\.Make\(.*map\[string\]' --type go
```

### 3b. Apply the transformation

Replace `map[string]string` with `map[string]any` in `Validate` and `Make` calls:

```diff
-ctx.Request().Validate(map[string]string{
+ctx.Request().Validate(map[string]any{
     "title": "required|max:255",
 })
```

Also update form request method signatures — `Rules()`, `Messages()`, `Attributes()`, and `Filters()`:

```diff
-func (r *UserCreate) Rules(ctx http.Context) map[string]string {
-    return map[string]string{
+func (r *UserCreate) Rules(ctx http.Context) map[string]any {
+    return map[string]any{
         "name": "required",
     }
 }

-func (r *UserCreate) Messages(ctx http.Context) map[string]string {
-    return map[string]string{}
+func (r *UserCreate) Messages(ctx http.Context) map[string]any {
+    return map[string]any{}
 }

-func (r *UserCreate) Filters(ctx http.Context) map[string]string {
-    return map[string]string{
+func (r *UserCreate) Filters(ctx http.Context) map[string]any {
+    return map[string]any{
         "name": "trim",
     }
 }
```

If a `regex` or `not_regex` rule value contains `|` and is followed by more rules, switch to `[]string`:

```diff
 rules := map[string]string{
-    "slug": "required|regex:^(news|docs)-[a-z]+$|string",
+    "slug": []string{"required", "regex:^(news|docs)-[a-z]+$", "string"},
 }
```

### 3c. Verification

```shell
# Ensure no remaining map[string]string in validation contexts
rg -n 'Validate\(.*map\[string\]string' --type go
rg -n 'Make\(.*map\[string\]string' --type go
rg -n 'Rules\(.*map\[string\]string' --type go
rg -n 'Messages\(.*map\[string\]string' --type go
rg -n 'Filters\(.*map\[string\]string' --type go

go build ./...
```

## Step 4: Update mail template content fields

Mail content fields are now explicit: `HtmlView` for HTML templates, `TextView` for plain-text templates, `Html`/`Text` for raw body content.

### 4a. Detect affected files

```shell
rg -n '\.View\s*:' --type go -g '!vendor/**' -g '!storage/**'
rg -n 'Content\(.*mail\.Content\{' --type go
rg -n '&mail\.Content\{' --type go
```

### 4b. Apply the transformation

For each `mail.Content{}` literal:

| Old field | New field | Condition |
|-----------|-----------|-----------|
| `View:` | `HtmlView:` | Always (was for HTML templates) |
| `Text:` (value ends in `.html`, `.txt`, `.tmpl`, or is a file path) | `TextView:` | When the value was a template path |
| `Text:` (value is a raw string, not a path) | `Text:` | Keep as-is for raw body |

**Example — template-based email**:
```diff
 Content(mail.Content{
-    View: "welcome.html",
-    Text: "welcome.txt",
+    HtmlView: "welcome.html",
+    TextView: "welcome.txt",
     With: map[string]any{
         "Name": "Goravel",
     },
 })
```

**Example — raw body email (no change)**:
```go
Content(mail.Content{Text: "Hello Goravel"})
```

If a file uses `Html:` / `Text:` already (raw body pattern from v1.17+), no change is needed.

### 4c. Verification

```shell
# Ensure no remaining View: in mail Content structs
rg -n '\.View\s*:.*\.html' --type go

go build ./...
```

## Step 5: Rename gRPC config key servers to clients

### 5a. Detect affected files

```shell
rg -n '"servers"' --type go -g '*grpc*'
rg -n 'grpc\.servers' --type go
```

### 5b. Apply the transformation

In `config/grpc.go`, rename the config key:

```diff
 config.Add("grpc", map[string]any{
     "host": config.Env("GRPC_HOST", ""),
-    "servers": map[string]any{
+    "clients": map[string]any{
         "user": map[string]any{
             "host":           config.Env("GRPC_USER_HOST", ""),
             ...
         },
     },
 })
```

In application code, update any config path references:

```diff
-facades.Config().Get("grpc.servers.user.host")
+facades.Config().Get("grpc.clients.user.host")
```

### 5c. Verification

```shell
# Ensure no remaining "servers" key in grpc config
rg -n '"servers"' --type go -g '*grpc*'
rg -n 'grpc\.servers' --type go

go build ./...
```

## Step 6: Update validation error messages and custom rule names

The new native validation engine ([goravel/framework#1397](https://github.com/goravel/framework/pull/1397)) produces Laravel-style error messages and evaluates all rules (including wildcards) differently. Any tests that assert on specific validation error messages or JSON error responses must be updated.

### 6a. Detect affected tests

```shell
rg -n 'Equal.*message.*required' --type go
rg -n 'Equal.*message.*regex' --type go
rg -n 'Equal.*message.*date' --type go
rg -n 'Required.*is required to not be empty' --type go
```

### 6b. Update error message expectations

Replace old-style error message assertions with the new Laravel-style messages:

| Old message | New message |
|-------------|-------------|
| `"name is required to not be empty"` | `"The name field is required."` |
| `"code value does not pass the regex check"` | `"The code field format is invalid."` |
| `"date value should be a date string"` | `"The date field must be a valid date."` |

Example test diff:

```diff
-s.Equal("{\"message\":{\"name\":{\"required\":\"name is required to not be empty\"}}}", content)
+s.Equal("{\"message\":{\"name\":{\"required\":\"The name field is required.\"}}}", content)
```

> **Note**: Wildcard rules (e.g., `scores.*`, `tags.*`) now consistently appear in error output even when no input is provided. Add assertions for these if your tests check the full error response body.

### 6c. Rename custom rules that conflict with built-in rules

The new engine includes built-in rules (`required`, `email`, `exists`, `unique`, etc.) that may conflict with custom rule names. Rename any custom rule whose `Signature()` matches a built-in rule name:

```diff
 func (receiver *Exists) Signature() string {
-    return "exists"
+    return "exists_custom"
 }
```

### 6d. Verification

```shell
go build ./...
go test ./... 2>&1 || true
```

## Step 7: Install the goravel-development skill

Download the Goravel AI coding conventions skill from goravel-lite so you can reference project structure, facades, testing patterns, and Artisan commands during the upgrade:

```shell
mkdir -p .agents/skills/goravel-development
curl -fsSL -o .agents/skills/goravel-development/SKILL.md \
  https://raw.githubusercontent.com/goravel/goravel-lite/master/.agents/skills/goravel-development/SKILL.md
```

### 7a. Verification

```shell
ls -l .agents/skills/goravel-development/SKILL.md
```

## Step 8: 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:
- Test middleware functions that were not migrated (step 2).
- Test validation calls using `map[string]string` (step 3).
- Test mail Content structs with old field names (step 4).
- Test validation error message assertions with old format (step 6).
- Custom rule signatures conflicting with built-in rule names (step 6).

Feature Introduction

AI SDK

Goravel v1.18 introduces a first-party AI SDK for conversations, attachments, generated media, and speech workflows through one facade. Install the facade with go run .artisan package:install AI, then install a provider package such as goravel/openai, goravel/anthropic, or goravel/gemini.

The initial release includes:

  • Agent conversations with make:agent, make:tool, customizable AI generator paths, Prompt, Stream, tool calling, prompt middleware, provider failover.
  • Request-scoped document and image attachments from bytes, readers, paths, storage, URLs, uploads, or provider-managed file IDs.
  • Image and audio generation with Store / StoreAs helpers, plus transcription with language and diarization options.

The goravel-lite scaffold also ships an AI Agent Development Skill which teaches AI coding agents (Cursor, Copilot, Codex, etc.) how to work with Goravel projects. The skill covers project structure, facades, routing, ORM, testing conventions, and Artisan scaffolding commands. When an AI agent reads this skill, it already knows how to bootstrap a Goravel project, use package:install to add facades, work with the facade pattern, write tests with testify suites and mock helpers, and use Artisan make:* generators. You can extend the skill with project-specific rules by creating .agents/skills/goravel-development/CUSTOM.md. The skill is versioned alongside goravel-lite and is updated when you upgrade the framework.

A minimal prompt looks like this:

go
conversation, err := facades.AI().Agent(&agents.SupportAgent{})
if err != nil {
    return err
}

response, err := conversation.Prompt("How do I create a controller?")
if err != nil {
    return err
}

fmt.Println(response.Text())

View Document

Telemetry

Goravel v1.18 introduces a telemetry module built on top of OpenTelemetry for collecting traces, metrics, and logs and exporting them to any OTLP-compatible backend, such as Jaeger, Prometheus, Grafana, or Datadog. Install the facade with go run . artisan package:install Telemetry.

The initial release includes:

  • Traces, metrics, and logs configured from a single config/telemetry.go file, with OTLP, console, and custom exporters, sampling, and batching controls.
  • Built-in instrumentation for the HTTP server, the HTTP client, gRPC, the database, and the logger, with trace context propagation across services.
  • Automatic flush and shutdown when the application stops.

A manual span looks like this:

go
ctx, span := facades.Telemetry().Tracer("app").Start(ctx, "process-order")
defer span.End()

span.SetAttributes(telemetry.String("order.id", "1234"))

// Pass ctx down to create child spans
processItems(ctx)

The Tracer argument ("app") is the instrumentation scope name that identifies the code producing the span; it is recorded as otel.scope.name. By convention it is the package import path or, for application code, the application name.

View Document

Laravel-style collections

Goravel now provides fluent eager and lazy collection utilities in the support/collect package. You can create collections from slices, chain filtering and mapping operations, use Laravel-style Where comparisons for structs, aggregate values, and process large datasets with LazyCollection.

go
numbers := collect.New(1, 2, 3, 4, 5, 6)

evens := numbers.Filter(func(n int, _ int) bool {
    return n%2 == 0
})

evens.All() // []int{2, 4, 6}

View Document

Native validation engine and built-in rules

Goravel now uses a native validation engine instead of the previous external validation dependency. The new engine supports dot notation, wildcard validation, validated data retrieval, custom filters, explicit message priority, and a broader Laravel-style built-in rule set.

go
validator, err := facades.Validation().Make(ctx,
    map[string]any{
        "email": " User@Goravel.dev ",
        "scores": []int{1, 2},
    },
    map[string]any{
        "email": "required|email",
        "scores.*": "required|integer",
    },
    validation.Filters(map[string]any{
        "email": "trim|lower",
    }),
)

validated := validator.Validated()
scores := validated["scores"].([]int)

View Document

Validation supports translation-based error messages and lang:publish command

Validation error messages are now powered by the framework's localization system. Default messages are embedded as lang/en/validation.json and use validation.* translation keys (e.g., validation.required, validation.min.string).

Use the new lang:publish Artisan command to publish the validation language files to your application:

shell
go run . artisan lang:publish
go run . artisan lang:publish --force

After publishing, edit lang/en/validation.json to customize default error messages. Create additional language directories (e.g., lang/zh/validation.json) for other locales. Custom rule messages now support :value and :optionN placeholders.

View Document

Maintenance mode commands

Goravel now provides the artisan down and artisan up commands for application maintenance mode. The file maintenance driver is used by default, so the down command writes maintenance metadata to storage and the up command removes it.

shell
go run . artisan down
go run . artisan up

The down command supports response customization with options such as --reason, --status, --redirect, --render, --secret, and --with-secret.

shell
go run . artisan down --reason="Upgrading database" --status=503
go run . artisan down --redirect=/maintenance
go run . artisan down --render=errors/503
go run . artisan down --with-secret

Maintenance settings are read from app.maintenance in config/app.go. New Route installations add this configuration automatically. Existing applications can add it manually:

go
"maintenance": map[string]any{
    "driver": config.Env("APP_MAINTENANCE_DRIVER", "file"),
    "store":  config.Env("APP_MAINTENANCE_STORE", ""),
},

For multi-server deployments, set APP_MAINTENANCE_DRIVER=cache to store maintenance state in a shared cache store. Use APP_MAINTENANCE_STORE to choose a named cache store, such as Redis:

ini
APP_MAINTENANCE_DRIVER=cache
APP_MAINTENANCE_STORE=redis

View Document

Runners can be disabled via config option

Goravel v1.18 replaces the internal app.auto_run flag with a public, glob-aware app.disabled_runners config option. It lets you selectively skip auto-run runners in the main process while keeping other services running — for example, run the scheduler on a dedicated container without disabling the HTTP server, or build a scheduler-only container by skipping goravel:http, goravel:queue, and goravel:grpc runners.

The value is a slice of glob patterns matched against each runner's signature with Go's path.Match:

go
// config/app.go
"app": map[string]any{
    "env":   "production",
    "debug": false,
    "disabled_runners": []string{"goravel:schedule"},
},

The framework runner signatures are goravel:http, goravel:grpc, goravel:queue, goravel:schedule, and goravel:telemetry. Patterns are evaluated in order and the first match wins, so you can mix exact and wildcard matches:

go
// Web container — only skip the scheduler
"disabled_runners": []string{"goravel:schedule"},

// Scheduler-only container — disable http, queue, grpc
"disabled_runners": []string{"goravel:http", "goravel:queue", "goravel:grpc"},

// Disable every framework runner
"disabled_runners": []string{"goravel:*"},

// Kill switch: run no auto-run runners in the main process
"disabled_runners": []string{"*"},

Invalid patterns are logged as warnings and the runner is left running, so a typo in the config never crashes the boot. The filter runs centrally in foundation.configureRunners(), so any new runner added in the future picks up the same behavior for free.

View Document

WithoutMiddleware for routes

Goravel v1.18 introduces the WithoutMiddleware method for both routes and route groups, similar to Laravel's withoutMiddleware(). This lets specific routes bypass certain middleware that would otherwise be applied by parent groups — useful for webhook endpoints, public APIs, or routes that should skip authentication or rate limiting.

Exclude middleware on individual routes from within a middleware group:

go
facades.Route().Middleware(middleware.Auth(), middleware.Throttle("global")).Group(func(router route.Router) {
  router.Get("/dashboard", dashboardController.Index)

  // This route excludes the throttle middleware
  router.Get("/api/webhook", webhookController.Handle).
    WithoutMiddleware(middleware.Throttle("global"))
})

Exclude middleware for all routes within a group:

go
facades.Route().Middleware(middleware.Auth()).
  WithoutMiddleware(middleware.Auth()).
  Group(func(router route.Router) {
    router.Get("/public", publicController.Index)
  })

Note: Middleware exclusion uses the Signature() method to identify middlewares. Make sure each middleware returns a unique signature for WithoutMiddleware to work correctly. The built-in framework middlewares already provide unique signatures.

View Document

Build command supports go generate

The build command now supports the --generate flag and -g alias. When enabled, Goravel runs go generate ./... before compiling the binary, making it easier to include generated code in the build workflow without changing the default behavior.

shell
go run . artisan build --generate
go run . artisan build -g

View Document

Artisan console supports table output

Goravel now provides the ctx.Table method for rendering structured command output. It accepts table headers, table rows, and an optional console.TableOption for border, style, column, width, and height customization.

go
func (receiver *ReportCommand) Handle(ctx console.Context) error {
    headers := []string{"ID", "Name", "Status"}
    rows := [][]string{
        {"1", "Goravel", "Active"},
        {"2", "Framework", "Ready"},
    }

    ctx.Table(headers, rows)

    return nil
}

View Document

Artisan console supports graceful shutdown via Shutdownable

Console commands can now opt into graceful shutdown by implementing the optional console.Shutdownable interface. The framework wires a signal.NotifyContext through every command, so pressing Ctrl+C (or sending SIGTERM) cancels the context passed to Handle. If the command also implements Shutdown(ctx Context) error, the framework calls it with a fresh context and a 30s budget for cleanup before the process exits. Handle runs in a goroutine, so the signal response is immediate.

console.Context now embeds context.Context, so commands can use <-ctx.Done() directly, pass ctx to functions that expect context.Context, and call ctx.Deadline() / ctx.Err() / ctx.Value(key) without an accessor. The schedule:run command is the canonical example — it implements Shutdown to forward cleanup to schedule.Shutdown(ctx).

go
package commands

import (
  "errors"
  "net/http"

  "github.com/goravel/framework/contracts/console"
  "github.com/goravel/framework/contracts/console/command"
)

type Serve struct {
  server *http.Server
}

func (r *Serve) Signature() string   { return "serve" }
func (r *Serve) Description() string { return "Start the HTTP server" }
func (r *Serve) Extend() command.Extend {
  return command.Extend{Category: "server"}
}

func (r *Serve) Handle(ctx console.Context) error {
  if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
    return err
  }
  return nil
}

func (r *Serve) Shutdown(ctx console.Context) error {
  ctx.Info("received signal, shutting down gracefully...")
  return r.server.Shutdown(ctx)
}

Commands that do not implement Shutdownable keep the original behavior — the process exits as soon as the signal is received.

View Document

Artisan console supports commands filtering

ApplicationBuilder now provides a WithCommandsFilter method to scope which built-in Artisan commands are registered — useful for production binaries that don't need make:*, package:*, or vendor:publish. The callback returns a positive list of command signatures to keep, with support for exact match and glob (*) matching:

go
func Boot() contractsfoundation.Application {
	return foundation.Setup().
		WithCommands(Commands).
		WithCommandsFilter(func() []string {
			if facades.Config().GetString("app.env") == "production" {
				return []string{
					"up", "down", "key:generate", "about",
					"schedule:*",
					"queue:*",
				}
			}
			return nil // keep everything in other environments
		}).
		WithConfig(config.Boot).
		Create()
}

View Document

gRPC supports TLS and mTLS credentials

Goravel now supports registering gRPC server and client transport credentials during application bootstrap. Use WithGrpcServerCredentials to enable TLS or mTLS for the server, and WithGrpcClientCredentials to register client credential groups that can be referenced by grpc.clients.<name>.credentials.

go
return foundation.Setup().
    WithGrpcClientCredentials(func() map[string]credentials.TransportCredentials {
        userCreds, err := credentials.NewClientTLSFromFile("storage/certs/ca.crt", "user.example.com")
        if err != nil {
            panic(err)
        }

        return map[string]credentials.TransportCredentials{
            "user": userCreds,
        }
    }).
    Create()

View Document

Orm query exposes context

Orm queries now provide a Context() accessor. Code that receives a contractsorm.Query, such as model global scopes, can read the context passed by facades.Orm().WithContext(ctx).

go
"tenant": func(query contractsorm.Query) contractsorm.Query {
    tenantID, _ := query.Context().Value("tenant_id").(uint)

    return query.Where("tenant_id", tenantID)
}

View Document

Package views via LoadViewsFrom

Extension packages can now register their own view directories with facades.View().LoadViewsFrom(path). The application's resources/views directory takes priority, and registered package views serve as defaults. The Exist method also checks registered package view paths.

go
// In an extension package's service_provider.go
func (r *ServiceProvider) Boot(app foundation.Application) {
    facades.View().LoadViewsFrom("/path/to/package/views")
}

View Document

Note: If you want to use this feature in the fiber driver, please check Fiber driver template configuration replaced with DefaultTemplate.

Package setup supports import aliases

Package setup modify helpers now support import aliases in the "<alias> <import-path>" format. This keeps generated bootstrap files readable when package import paths are long or when the package should be referenced with a custom name.

go
serviceProvider := "&admin.ServiceProvider{}"
moduleImport := "admin " + setup.Paths().Module().Import()

setup.Install(
    modify.RegisterProvider(moduleImport, serviceProvider),
).Uninstall(
    modify.UnregisterProvider(moduleImport, serviceProvider),
).Execute()

View Document

Queue drivers support batch-consuming via DriverWithReceive

Custom queue drivers can now optionally implement the DriverWithReceive interface (goravel/framework#1435) to support blocking batch message consumption. When a driver implements this interface, the queue worker automatically uses Receive instead of Pop, reducing polling overhead for high-throughput drivers like Kafka or RabbitMQ.

go
import (
  "context"
  "github.com/goravel/framework/contracts/queue"
)

type KafkaDriver struct{}

// Base Driver interface methods (required)
func (d *KafkaDriver) Driver() string                        { return "kafka" }
func (d *KafkaDriver) Push(task queue.Task, queue string) error { return nil }
func (d *KafkaDriver) Pop(queue string) (queue.ReservedJob, error) { return nil, nil }

// Optional DriverWithReceive for batch consumption
func (d *KafkaDriver) Receive(ctx context.Context, queue string, count int) ([]queue.ReservedJob, error) {
  // blocking batch receive up to `count` jobs, returns when ctx expires
}

When Receive is available, the worker runs a blocking batch loop with a 5-second per-call timeout and exponential backoff (100ms–3.2s) on errors or empty batches. The context.Context is canceled on worker shutdown, ensuring clean termination. No application code changes are required for existing drivers.

View Document

Log WithContext filters framework context keys out

Previously, facades.Log().WithContext(ctx) could write framework-internal context keys such as GoravelAuthJwt and goravel_http_client_name to log output. Goravel now excludes these keys by default, and you can add logging.context.exclude for project-specific context keys.

View Document

Installer supports agent skill management commands

The goravel CLI now supports goravel skill:list and goravel skill:install for discovering and installing Goravel agent skills. These commands are designed for contributors who want to install AI coding assistant skills to help follow Goravel development conventions such as testing, naming, and pull request workflows.

Skills are sourced from the goravel/agents repository and installed to ~/.agents/skills by default. You can filter by name, choose a custom destination with --path, and overwrite existing skills with --force.

View Document

Fiber driver context decoupled from Fiber instance lifecycle

When a request timed out with the Fiber driver, the context.Context interface methods (Done(), Deadline(), Err()) delegated to the underlying fiber.Ctx, which became invalid after the request was recycled by fasthttp. This caused nil-pointer panics when background goroutines (e.g. database/sql) still held a reference to the request context (goravel/goravel#951).

The fix (goravel/fiber#253) stores the user context and values as struct fields on the goravel Context struct instead of delegating to fiber.Ctx, so timeout signals propagate correctly and background goroutines can safely access the context after the request returns.

No application code changes are required.

Fiber driver template configuration replaced with DefaultTemplate

To support package views via LoadViewsFrom, the Fiber driver (goravel/fiber#273) has replaced its dependency on gofiber/template/html/v3 with a built-in DefaultTemplate() that loads views from resources/views and any registered package views automatically. If your config/http.go has a custom template configuration using html.New(path.Resource("views"), ".tmpl") and you want to use the Package views via LoadViewsFrom feature, update it to use goravelfiber.DefaultTemplate() instead:

diff
 // config/http.go — fiber driver

import goravelfiber "github.com/goravel/fiber"

 "template": func() (fiber.Views, error) {
-    return html.New(path.Resource("views"), ".tmpl"), nil
+    return goravelfiber.DefaultTemplate(), nil
 },

For custom delimiters or FuncMap, use goravelfiber.NewTemplate(goravelfiber.RenderOptions{...}):

go
import goravelfiber "github.com/goravel/fiber"

"template": func() (fiber.Views, error) {
    return goravelfiber.NewTemplate(goravelfiber.RenderOptions{
        Delims: &goravelfiber.Delims{Left: "{[", Right: "]}"},
    })
},

If no template configuration is defined, DefaultTemplate() is used automatically — no code change is required.

Cache prefix supports empty string

The Redis cache driver now respects an empty prefix configuration. Previously, even with an empty prefix, a colon (:) separator was always prepended to every key, making keys inconsistent when accessing Redis directly. Now, if the prefix option in config/cache.go is set to an empty string, no prefix or separator is applied:

go
// config/cache.go
"prefix": "",

With this configuration, a cache key like users is stored as users in Redis, rather than :users.

No application code changes are required for existing projects with a non-empty prefix.

View Document

HTTP Middleware changed from func to interface

contracts/http.Middleware has changed from a function type func(Context) to an interface:

go
type Middleware interface {
    Handle(Context)
    Signature() string
}

This change provides each middleware with a stable identity via the Signature() method, enabling WithoutMiddleware to reliably exclude specific middleware by signature comparison. All built-in framework middlewares (Cors, Timeout, Throttle, VerifyCsrfToken, etc.) have been updated to return structs that implement this interface.

Custom middleware must be updated to implement the new interface. See the upgrade guide for migration steps.

View Document

Validation engine rewritten natively

The validation engine was rewritten in goravel/framework#1397 — replacing the external gookit/validate dependency with a native implementation that is fully compatible with Laravel's validation API. This change introduces several downstream effects:

  1. Error message format changed from old-style messages (e.g., "name is required to not be empty") to Laravel-style messages (e.g., "The name field is required."). Tests that assert on specific validation error messages must be updated.

  2. Wildcard rules (*.) now consistently report errors. The old engine might have skipped wildcard-field errors depending on input; the new engine always evaluates and reports them.

  3. Form request method signatures changedRules(), Messages(), Attributes(), and Filters() now return map[string]any instead of map[string]string.

  4. Custom validation rule names may conflict with new built-in rules. Rename custom rules that share names with built-in rules (e.g., existsexists_custom).

Error messages are now powered by the localization system. Default messages are embedded as lang/en/validation.json and use validation.* translation keys. Use go run . artisan lang:publish to publish and customize them.

View Document

Mail content separates raw text from template views

Goravel now treats mail.Content.Text as raw plain-text email body content. Template paths are explicit: use HtmlView for HTML templates and TextView for plain-text templates.

Use Html and Text when the message body is already available:

go
func (m *WelcomeMail) Content() *mail.Content {
    return &mail.Content{
        Html: "<h1>Hello Goravel</h1>",
        Text: "Hello Goravel",
    }
}

Use HtmlView and TextView when the message body should be rendered from templates:

go
func (m *WelcomeMail) Content() *mail.Content {
    return &mail.Content{
        HtmlView: "welcome.html",
        TextView: "welcome.txt",
        With: map[string]any{
            "Name": "Goravel",
        },
    }
}

View Document

gRPC config key renamed from servers to clients

The gRPC configuration key servers has been renamed to clients in config/grpc.go to accurately reflect that these entries register gRPC clients (consumers of remote services) rather than servers.

In config/grpc.go, rename the top-level key:

diff
 config.Add("grpc", map[string]any{
     "host": config.Env("GRPC_HOST", ""),
-    "servers": map[string]any{
+    "clients": map[string]any{
         "user": map[string]any{
             "host": config.Env("GRPC_USER_HOST", ""),
             ...
         },
     },
 })

In application code, update any config path references from grpc.servers to grpc.clients:

diff
-facades.Config().Get("grpc.servers.user.host")
+facades.Config().Get("grpc.clients.user.host")

View Document

HTTP driver timeout middleware rewritten for concurrent safety

The timeout middleware in both the Gin driver (goravel/gin#222) and the Fiber driver (goravel/fiber#263) has been rewritten to fix a concurrency bug (goravel#966). The previous implementation ran the handler chain in a background goroutine and could return a timeout response while the handler kept running on request-scoped state that was already eligible for reuse, causing cross-request response contamination under high concurrency.

Both drivers now delegate to their framework's native timeout middleware (gin-contrib/timeout for Gin, Fiber's built-in timeout for Fiber), which properly abandons timed-out request contexts instead of recycling them. The Timeout(...) middleware signature is unchanged and ctx.Done() still reflects request deadlines — no application code changes are required.

View Document

Released under the MIT License