Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
200c19b
added non-zero exit status support for returned command errors
ganigeorgiev Aug 1, 2026
f9795da
added hasArchiveExtension util
ganigeorgiev Aug 2, 2026
4c6c166
order new files at the end
ganigeorgiev Aug 2, 2026
b259534
added quotes for the default Content-Disposition serving filename in …
ganigeorgiev Aug 2, 2026
dc7482e
[#7785] fixed realtime API preview examples
ganigeorgiev Aug 3, 2026
32fa2dd
added filesystem.NewWriter helper
ganigeorgiev Aug 3, 2026
9f1a4bc
minor ui base component fixes and shablon update
ganigeorgiev Aug 10, 2026
2262bb4
merged with master
ganigeorgiev Aug 10, 2026
a4f1fa3
added Cross-Origin-Opener-Policy:same-origin to the default security …
ganigeorgiev Aug 11, 2026
c86fa88
Merge branch 'master' into develop
ganigeorgiev Aug 12, 2026
1247b24
synced with master
ganigeorgiev Aug 14, 2026
4d4275c
updated custom select class position
ganigeorgiev Aug 16, 2026
97dd775
(experimental) upgraded to encoding/json/v2
ganigeorgiev Aug 17, 2026
14fc20a
bumped go github action to 1.27rc3
ganigeorgiev Aug 18, 2026
bad7cc0
updated go version in go.mod
ganigeorgiev Aug 18, 2026
f5695a7
bumped app version
ganigeorgiev Aug 18, 2026
25081f1
make raw json scanning determinisctic and added setting option to lim…
ganigeorgiev Aug 18, 2026
1a5f298
split changelogs
ganigeorgiev Aug 18, 2026
e71be71
added logs truncate endpoint
ganigeorgiev Aug 19, 2026
c2cbd97
use fixed message limit seperate from the log data
ganigeorgiev Aug 19, 2026
252358e
use app.pb.logs.truncate()
ganigeorgiev Aug 19, 2026
50a7700
bumped go github action version to 1.27, updated modernc.org/sqlite a…
ganigeorgiev Aug 19, 2026
bf1f164
added Record.GetInt64(field) helper
ganigeorgiev Aug 19, 2026
25dc8a8
added Store.Keys()
ganigeorgiev Aug 21, 2026
0e9dcfe
added experimental backup optimizations
ganigeorgiev Aug 21, 2026
654af12
unexport the filesystem hooks for now to avoid introducing breaking c…
ganigeorgiev Aug 21, 2026
80608e8
updated jstypes
ganigeorgiev Aug 21, 2026
f544fa0
updated tests
ganigeorgiev Aug 22, 2026
b648b95
fix some of the flaky tests
ganigeorgiev Aug 22, 2026
5e5ba38
comment fieldsWithExcerpt to make use of the faster streaming json path
ganigeorgiev Aug 22, 2026
7f4b532
updated backport changelog
ganigeorgiev Aug 22, 2026
a293e13
use the concurrent db pool for the backup db copy
ganigeorgiev Aug 22, 2026
18cb3dc
ugnore dirs to avoid unnecessery logs
ganigeorgiev Aug 22, 2026
eade68d
check for isdir error before logging
ganigeorgiev Aug 22, 2026
c532fd1
wrapped backup zip copy operations in a mutex since the file hook can…
ganigeorgiev Aug 22, 2026
eae29c8
updated linter and bumped app version
ganigeorgiev Aug 22, 2026
129d46f
fixed changelog typo
ganigeorgiev Aug 22, 2026
9e15b46
fixed err variable name
ganigeorgiev Aug 22, 2026
70d5857
wrap zip close with mutex
ganigeorgiev Aug 22, 2026
50f5f83
added closed checks as an extra precaution
ganigeorgiev Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
node-version: '>=25.2.1'

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: '>=1.26.6'
go-version: '>=1.27.0'

# This step usually is not needed because the /ui/dist is pregenerated locally
# but its here to ensure that each release embeds the latest admin ui artifacts.
Expand Down
1,251 changes: 23 additions & 1,228 deletions CHANGELOG.md

Large diffs are not rendered by default.

File renamed without changes.
5 changes: 5 additions & 0 deletions CHANGELOG_16_22.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---

## v0.22.53

- Workaround `encoding/json` infinite recursion when used with Go 1.27 ([#7808](https://github.com/pocketbase/pocketbase/issues/7808)).


## v0.22.52

- (_Backported from v0.39.11_) Bumped `golang.org/x/*` deps and the min Go GitHub action version to 1.26.6 because it comes with some [minor bug and security fixes](https://github.com/golang/go/issues?q=milestone%3AGo1.26.6).
Expand Down
1,243 changes: 1,243 additions & 0 deletions CHANGELOG_23_39.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This document describes how to prepare a PR for a change in the main repository.

## Prerequisites

- Go 1.25+ (for making changes in the Go code)
- Go 1.27+ (for making changes in the Go code)
- Node 24+ (for making changes in the Superuser UI)

If you haven't already, you can fork the main repository and clone your fork so that you can work locally:
Expand Down
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ your own custom app specific business logic and still have a single portable exe

Here is a minimal example:

0. [Install Go 1.25+](https://go.dev/doc/install) (_if you haven't already_)
0. [Install Go 1.27+](https://go.dev/doc/install) (_if you haven't already_)

1. Create a new project directory with the following `main.go` file inside it:
```go
Expand Down Expand Up @@ -92,32 +92,37 @@ _For more details please refer to [Extend with Go](https://pocketbase.io/docs/go

To build the minimal standalone executable, like the prebuilt ones in the releases page, you can simply run `go build` inside the `examples/base` directory:

0. [Install Go 1.25+](https://go.dev/doc/install) (_if you haven't already_)
0. [Install Go 1.27+](https://go.dev/doc/install) (_if you haven't already_)
1. Clone/download the repo
2. Navigate to `examples/base`
3. Run `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build`
(_https://go.dev/doc/install/source#environment_)
3. Run `CGO_ENABLED=0 go build` to build a binary for your current environment
_(or to target other platforms use `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build`; see https://go.dev/doc/install/source#environment)_
4. Start the created executable by running `./base serve`.

Note that the supported build targets by the pure Go SQLite driver at the moment are:

```
darwin amd64
darwin arm64
freebsd amd64
freebsd arm64
linux 386
linux amd64
linux arm
linux arm64
linux loong64
linux ppc64le
linux riscv64
linux s390x
windows 386
windows amd64
windows arm64
```
| GOOS | GOARCH |
|---------|---------|
| darwin | amd64 |
| darwin | arm64 |
| freebsd | 386 |
| freebsd | amd64 |
| freebsd | arm |
| freebsd | arm64 |
| linux | 386 |
| linux | amd64 |
| linux | arm |
| linux | arm64 |
| linux | loong64 |
| linux | ppc64le |
| linux | riscv64 |
| linux | s390x |
| netbsd | amd64 |
| openbsd | amd64 |
| openbsd | arm64 |
| windows | 386 |
| windows | amd64 |
| windows | arm64 |

### Testing

Expand All @@ -134,7 +139,7 @@ Check also the [Testing guide](http://pocketbase.io/docs/testing) to learn how t

If you discover a security vulnerability within PocketBase, please send an e-mail to **support at pocketbase.io**.

All reports will be promptly addressed and you'll be credited in the fix release notes.
You could find more details in the project [Security policy](https://github.com/pocketbase/pocketbase/security/policy).

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion apis/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package apis

import (
"bytes"
"encoding/json"
"encoding/json/v2"
"errors"
"io"
"mime/multipart"
Expand Down
2 changes: 1 addition & 1 deletion apis/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1617,7 +1617,7 @@ func TestCollectionOAuth2Providers(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{
`{"name":"oidc3","displayName":"OIDC","logo":"\u003csvg`,
`{"name":"oidc3","displayName":"OIDC","logo":"<svg`,
},
NotExpectedContent: []string{
`"order":`,
Expand Down
7 changes: 3 additions & 4 deletions apis/cron_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ func TestCronsRun(t *testing.T) {
app.Cron().Add("test", "* * * * *", func() {
app.Store().Set("testJobCalls", cast.ToInt(app.Store().Get("testJobCalls"))+1)
})
app.Cron().Stop()
}

expectedCalls := func(expected int) func(t testing.TB, app *tests.TestApp, res *http.Response) {
return func(t testing.TB, app *tests.TestApp, res *http.Response) {
time.Sleep(50 * time.Millisecond)

total := cast.ToInt(app.Store().Get("testJobCalls"))
if total != expected {
t.Fatalf("Expected total testJobCalls %d, got %d", expected, total)
Expand All @@ -93,7 +96,6 @@ func TestCronsRun(t *testing.T) {
Name: "unauthorized",
Method: http.MethodPost,
URL: "/api/crons/test",
Delay: 50 * time.Millisecond,
BeforeTestFunc: beforeTestFunc,
AfterTestFunc: expectedCalls(0),
ExpectedStatus: 401,
Expand All @@ -107,7 +109,6 @@ func TestCronsRun(t *testing.T) {
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
},
Delay: 50 * time.Millisecond,
BeforeTestFunc: beforeTestFunc,
AfterTestFunc: expectedCalls(0),
ExpectedStatus: 403,
Expand All @@ -121,7 +122,6 @@ func TestCronsRun(t *testing.T) {
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
Delay: 50 * time.Millisecond,
BeforeTestFunc: beforeTestFunc,
AfterTestFunc: expectedCalls(0),
ExpectedStatus: 404,
Expand All @@ -135,7 +135,6 @@ func TestCronsRun(t *testing.T) {
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
Delay: 50 * time.Millisecond,
BeforeTestFunc: beforeTestFunc,
AfterTestFunc: expectedCalls(1),
ExpectedStatus: 204,
Expand Down
23 changes: 22 additions & 1 deletion apis/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import (

// bindLogsApi registers the request logs api endpoints.
func bindLogsApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
sub := rg.Group("/logs").Bind(RequireSuperuserAuth(), SkipSuccessActivityLog())
sub := rg.Group("/logs").Bind(
RequireSuperuserAuth(),
SkipSuccessActivityLog(),
)

sub.GET("", logsList)
sub.DELETE("", logsTruncate)
sub.GET("/stats", logsStats)
sub.GET("/{id}", logsView)
}
Expand Down Expand Up @@ -71,3 +76,19 @@ func logsView(e *core.RequestEvent) error {

return e.JSON(http.StatusOK, log)
}

func logsTruncate(e *core.RequestEvent) error {
// delete all rows directly (aka. no model hooks will be fired)
_, err := e.App.AuxNonconcurrentDB().Delete((&core.Log{}).TableName(), nil).Execute()
if err != nil {
return e.InternalServerError("Failed to truncate all logs.", err)
}

// try to free the unused disk space
err = e.App.AuxVacuum()
if err != nil {
e.App.Logger().Warn("Failed to VACUUM aux database", "error", err)
}

return e.NoContent(http.StatusNoContent)
}
56 changes: 56 additions & 0 deletions apis/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,59 @@ func TestLogsStats(t *testing.T) {
scenario.Test(t)
}
}

func TestLogsTruncate(t *testing.T) {
t.Parallel()

scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodDelete,
URL: "/api/logs",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authorized as regular user",
Method: http.MethodDelete,
URL: "/api/logs",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authorized as superuser",
Method: http.MethodDelete,
URL: "/api/logs",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
if err := tests.StubLogsData(app); err != nil {
t.Fatal(err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
var found []core.Log

if err := app.LogQuery().All(&found); err != nil {
t.Fatal(err)
}

if len(found) > 0 {
t.Fatalf("Expected all logs to be deleted, found: %v", found)
}
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{"*": 0},
},
}

for _, scenario := range scenarios {
scenario.Test(t)
}
}
1 change: 1 addition & 0 deletions apis/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ func securityHeaders() *hook.Handler[*core.RequestEvent] {
e.Response.Header().Set("X-XSS-Protection", "1; mode=block")
e.Response.Header().Set("X-Content-Type-Options", "nosniff")
e.Response.Header().Set("X-Frame-Options", "SAMEORIGIN")
e.Response.Header().Set("Cross-Origin-Opener-Policy", "same-origin")

// @todo consider a default HSTS?
// (see also https://webkit.org/blog/8146/protecting-against-hsts-abuse/)
Expand Down
20 changes: 12 additions & 8 deletions apis/middlewares_rate_limit_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package apis_test

import (
"fmt"
"net/http/httptest"
"testing"
"testing/synctest"
"time"

"github.com/pocketbase/pocketbase/apis"
Expand Down Expand Up @@ -127,38 +129,40 @@ func TestDefaultRateLimitMiddleware(t *testing.T) {
{"/rate/guest", 0, true, 429},
}

for _, s := range scenarios {
t.Run(s.url, func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
for i, s := range scenarios {
prefix := fmt.Sprintf("[%s:%d] ", s.url, i+1)

rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", s.url, nil)

if s.authenticated {
auth, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
t.Fatalf(prefix+"%v", err)
}

token, err := auth.NewAuthToken()
if err != nil {
t.Fatal(err)
t.Fatalf(prefix+"%v", err)
}

req.Header.Add("Authorization", token)
}

if s.wait > 0 {
time.Sleep(time.Duration(s.wait) * time.Millisecond)
synctest.Sleep(time.Duration(s.wait) * time.Millisecond)
}

mux.ServeHTTP(rec, req)

result := rec.Result()

if result.StatusCode != s.expectedStatus {
t.Fatalf("Expected response status %d, got %d", s.expectedStatus, result.StatusCode)
t.Fatalf(prefix+"Expected response status %d, got %d", s.expectedStatus, result.StatusCode)
}
})
}
}
})
}

func TestDefaultRateLimitMiddlewareSkipChecks(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion apis/realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package apis

import (
"context"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"log/slog"
Expand Down
6 changes: 4 additions & 2 deletions apis/realtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package apis_test

import (
"context"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -1171,7 +1171,9 @@ func TestRealtimeRecordResolve(t *testing.T) {

var mu sync.Mutex
notify := func(clientId string, eventData []byte) {
data := struct{ Action string }{}
data := struct {
Action string `json:"action"`
}{}
_ = json.Unmarshal(eventData, &data)

mu.Lock()
Expand Down
2 changes: 1 addition & 1 deletion apis/record_auth_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestRecordAuthMethodsList(t *testing.T) {
`"providers":[{`,
`"name":"google"`,
`"name":"gitlab"`,
`"logo":"\u003csvg`,
`"logo":"<svg`,
`"logo":""`, // for the legacy fields
`"state":`,
`"displayName":`,
Expand Down
2 changes: 1 addition & 1 deletion apis/record_auth_with_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bytes"
"context"
"database/sql"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"io"
Expand Down
2 changes: 1 addition & 1 deletion apis/record_auth_with_oauth2_redirect.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package apis

import (
"encoding/json"
"encoding/json/v2"
"errors"
"net/http"
"strings"
Expand Down
Loading
Loading