For the Go version please refer to /v023upgrade/go.

    Please note that you don't have to upgrade to v0.23.0 if you are not planning further developing your existing app or are satisfied with the v0.22.x features set. There are no critical issues with PocketBase v0.22.x and in the case of reported bugs and security vulnerabilities, the fixes will be backported for at least until Q1 of 2025 (if not longer).

    If you don't plan upgrading just make sure to pin the SDKs version to their latest PocketBase v0.22.x compatible:

    • JS SDK: <0.22.0
    • Dart SDK: <0.19.0

    Before upgrading make sure to backup your current pb_data directory somewhere safe.

    PocketBase v0.23+ comes with many changes and requires applying manual migration steps for the pb_hooks and pb_migrations.
    Existing data in pb_data/data.db will be automatically upgraded once the PocketBase v0.23.0 executable is started.
    But there is no easy and automated code upgrade solution so be prepared because it might take you some time - from 1h to entire weekend!

    Suggested upgrade path:

    1. Download a backup of your production pb_data so that you can adjust your code and test the upgrade locally.
    2. Ensure that you are already using PocketBase v0.22.x (you can find the version number in the bottom-right corner in the UI). If you are using an older version, please upgrade first to PocketBase v0.22.x (there are no major breaking changes in the recent v0.22.x releases so it should be enough to download the specific executable and run it against your pb_data).
    3. If you are using the SDKs, apply the necessary changes to your client-side code by following:
    4. If your existing pb_migrations files are all autogenerated, you may find it easier to upgrade by deleting all of them and run with the PocketBase v0.23.0 executable ./pocketbase migrate collections which will create a full snapshot of your latest collections configuration.
      Optionally, after that you can also run ./pocketbase migrate history-sync to remove the deleted migrations from the applied migrations history.
    5. Apply the necessary pb_hooks code changes based on the notes listed below in this document.
    6. Start PocketBase v0.23.0 as usual and test your local changes (including a test against no existing pb_data).
    7. If everything is working fine, create another more recent production backup for just in case.
      After that you can deploy your PocketBase v0.23.0 executable, together with the upgraded client-side code, pb_hooks and pb_migrations to your production server.

    The below listed changes doesn't cover everything but it should be a good starting point.

    If you need help with the "translation" of your old code to the new APIs, feel free to open a Q&A discussion with the specific code sample and I'll try to assist you.

    For consistency with the Go standard library and to silent commonly used linters the following initilisms case normalization was applied for all exported bindings:

    Old New
    %Json% %JSON%
    %Ip% %IP%
    %Url% %URL%
    %Smtp% %SMTP%
    %Tls% %TLS%
    %Jwt% %JWT%

    The Dao abstraction has been removed and most of the old Dao methods are now part of the $app instance.

    The app settings related to the OAuth2 and email templates are moved in the collection options to allow more granular customizations.

    For more details about all new app fields and methods, please refer to the $app.* docs in the JSVM reference.

    $app.cache()
    $app.store()
    $app.refreshSettings()
    $app.reloadSettings()
    $app.dao().adminQuery()
    $app.recordQuery("_superusers")
    $app.dao().findAdminById(id)
    $app.findRecordById("_superusers", id)
    $app.dao().findAdminByEmail(email)
    $app.findAuthRecordByEmail("_superusers", email)
    $app.dao().findAdminByToken(token, baseTokenKey)
    // tokenType could be: // - auth // - file // - verification // - passwordReset // - emailChange $app.findAuthRecordByToken(token, [tokenType])
    $app.dao().totalAdmins()
    $app.countRecords("_superusers", [exprs...])
    $app.dao().deleteAdmin(admin)
    $app.delete(admin)
    $app.dao().saveAdmin(admin)
    $app.save(admin)
    $app.dao().recordQuery(collectionModelOrIdentifier)
    $app.recordQuery(collectionModelOrIdentifier)
    $app.dao().findRecordById(collectionNameOrId, recordId, [optFilters...])
    $app.findRecordById(collectionModelOrIdentifier, recordId, [optFilters...])
    $app.dao().findRecordsByIds(collectionNameOrId, recordIds, [optFilters...])
    $app.findRecordsByIds(collectionModelOrIdentifier, recordIds, [optFilters...])
    $app.dao().findRecordsByExpr(collectionNameOrId, [exprs...])
    $app.findAllRecords(collectionModelOrIdentifier, [exprs...])
    $app.dao().findFirstRecordByData(collectionNameOrId, key, value)
    $app.findFirstRecordByData(collectionModelOrIdentifier, key, value)
    $app.dao().findRecordsByFilter(collectionNameOrId, filter, sort, limit, offset, [params])
    app.findRecordsByFilter(collectionModelOrIdentifier, filter, sort, limit, offset, [params])
    N/A
    $app.countRecords(collectionModelOrIdentifier, [exprs...])
    $app.dao().findAuthRecordByToken(token, baseTokenKey)
    // tokenType could be: // - auth // - file // - verification // - passwordReset // - emailChange $app.findAuthRecordByToken(token, [tokenType])
    $app.dao().findAuthRecordByEmail(collectionNameOrId, email)
    $app.findAuthRecordByEmail(collectionModelOrIdentifier, email)
    $app.dao().findAuthRecordByUsername(collectionNameOrId, username)
    // The "username" field is no longer required and has no special meaning. $app.findFirstRecordByData(collectionModelOrIdentifier, "username", username)
    $app.dao().suggestUniqueAuthRecordUsername(collectionNameOrId, baseUsername, [excludeIds...])
    // The "username" field is no longer required and has no special meaning. As a workaround you can use something like: function suggestUniqueAuthRecordUsername( collectionModelOrIdentifier, baseUsername, ) { let username = baseUsername for (i = 0; i < 10; i++) { // max 10 attempts try { let total = $app.countRecords( collectionModelOrIdentifier, $dbx.exp("LOWER([[username]])={:username}", {"username": username.toLowerCase()}), ) if (total == 0) { break // already unique } } catch {} username = baseUsername + $security.randomStringWithAlphabet(3+i, "123456789") } return username }
    $app.dao().canAccessRecord(record, requestInfo, accessRule)
    $app.canAccessRecord(record, requestInfo, accessRule)
    $app.dao().expandRecord(record, expands, optFetchFunc)
    $app.expandRecord(record, expands, optFetchFunc)
    $app.dao().expandRecords(records, expands, optFetchFunc)
    $app.expandRecords(records, expands, optFetchFunc)
    $app.dao().saveRecord(record)
    $app.save(record)
    $app.dao().deleteRecord(record)
    $app.delete(record)
    $app.dao().findAllExternalAuthsByRecord(authRecord)
    $app.findAllExternalAuthsByRecord(authRecord)
    N/A
    $app.findAllExternalAuthsByCollection(collection)
    $app.dao().findFirstExternalAuthByExpr(expr)
    $app.findFirstExternalAuthByExpr(expr)
    $app.dao().findExternalAuthByRecordAndProvider(authRecord)
    $app.findFirstExternalAuthByExpr($dbx.hashExp({ "collectionRef": authRecord.collection().id, "recordRef": authRecord.id, "provider": provider, }))
    $app.dao().deleteExternalAuth(model)
    $app.delete(model)
    $app.dao().saveExternalAuth(model)
    $app.save(model)
    $app.dao().collectionQuery()
    $app.collectionQuery()
    $app.dao().findCollectionsByType(collectionType)
    $app.findAllCollections(collectionTypes...)
    $app.dao().findCollectionByNameOrId(nameOrId)
    $app.findCollectionByNameOrId(nameOrId)
    $app.dao().isCollectionNameUnique(name, excludeIds...)
    $app.isCollectionNameUnique(name, excludeIds...)
    $app.dao().findCollectionReferences(collection, excludeIds...)
    $app.findCollectionReferences(collection, excludeIds...)
    $app.dao().deleteCollection(collection)
    $app.delete(collection)
    $app.dao().saveCollection(collection)
    $app.save(collection)
    $app.dao().importCollections(importedCollections, deleteMissing, afterSync)
    $app.importCollections(importSliceOfMap, deleteMissing)
    N/A
    $app.importCollectionsByMarshaledJSON(sliceOfMapsBytes, deleteMissing)
    $app.dao().syncRecordTableSchema(newCollection, oldCollection)
    $app.syncRecordTableSchema(newCollection, oldCollection)
    $app.dao().logQuery()
    $app.logQuery()
    $app.dao().findLogById(id)
    $app.findLogById(id)
    $app.dao().logsStats(expr)
    $app.logsStats(expr)
    $app.dao().deleteOldLogs(createdBefore)
    $app.deleteOldLogs(createdBefore)
    $app.dao().saveLog(log)
    $app.auxSave(log)
    $app.dao().saveSettings(newSettings, [optEncryptionKey])
    $app.save(newSettings)
    const titles = ["title1", "title2", "title3"] const collection = $app.dao().findCollectionByNameOrId("articles") $app.dao().runInTransaction((txDao) => { // create new record for each title for (let title of titles) { const record = new Record(collection) record.set("title", title) txDao.saveRecord(record) } })
    const titles = ["title1", "title2", "title3"] const collection = $app.findCollectionByNameOrId("articles") $app.runInTransaction((txApp) => { // create new record for each title for (let title of titles) { const record = new Record(collection) record.set("title", title) txApp.saveRecord(record) } })
    $app.dao().vacuum()
    $app.vacuum() / $app.auxVacuum()
    $app.dao().hasTable(tableName)
    $app.hasTable(tableName)
    $app.dao().tableColumns(tableName)
    $app.tableColumns(tableName)
    $app.dao().tableInfo(tableName)
    $app.tableInfo(tableName)
    $app.dao().tableIndexes(tableName)
    $app.tableIndexes(tableName)
    $app.dao().deleteTable(tableName)
    $app.deleteTable(tableName)
    $app.dao().deleteView(name)
    $app.deleteView(name)
    $app.dao().saveView(name, selectQuery)
    $app.saveView(name, selectQuery)
    $app.dao().createViewSchema(selectQuery)
    $app.createViewFields(selectQuery)

    The Record create/update operations are simplified and the Dao and RecordUpsertForm abstractions have been removed.

    $app.save(record) both validate and persist the record. Use $app.saveNoValidate(record) if you want to skip the validations.

    Files are treated as regular field value, so for uploading a new record file is enough to call record.set("yourFileField", $filesystem.fileFromPath("/path/to/file1")).

    Additionally, the record.set() method now supports all field modifiers that are available with the Web APIs. For example: record.set("total+", 10) adds 10 to the existing total record field value. More information about the available modifiers you can find in the main documentation.

    const record = $app.dao().findRecordById("articles", "RECORD_ID") const form = new RecordUpsertForm($app, record) form.loadData({ "title": "Lorem ipsum", "active": true, }) // upload file(s) const f1 = $filesystem.fileFromPath("/path/to/file1") const f2 = $filesystem.fileFromPath("/path/to/file2") form.addFiles("documents", f1, f2) // mark file(s) for deletion form.removeFiles("featuredImages", "demo_xzihx0w.png") form.submit()
    const record = $app.findRecordById("articles", "RECORD_ID") // there is also record.load({ ... }) record.set("title", "Lorem ipsum") record.set("active", true) // upload file(s) record.set("documents", [ $filesystem.fileFromPath("/path/to/file1"), $filesystem.fileFromPath("/path/to/file2"), ]) // mark file(s) for deletion record.set("featuredImages-", "demo_xzihx0w.png") $app.save(record)
    record.schemaData()
    record.fieldsData()
    record.unknownData()
    record.customData()
    record.withUnknownData(state)
    record.withCustomData(state)
    record.originalCopy()
    record.original()
    record.cleanCopy()
    record.fresh()
    record.getTime(fieldName)
    record.getDateTime(fieldName).Time()
    $tokens.recordAuthToken($app, record)
    record.newAuthToken()
    $tokens.recordVerifyToken($app, record)
    record.newVerificationToken()
    $tokens.recordResetPasswordToken($app, record)
    record.newPasswordResetToken()
    $tokens.recordChangeEmailToken($app, record, newEmail)
    record.newEmailChangeToken()
    $tokens.recordFileToken($app, record)
    record.NewFileToken()

    Added new dedicated collection type constructors: new BaseCollection(name) new AuthCollection(name) new ViewCollection(name)

    Collection.schema is now a regular array of fields and the model field is renamed to Collection.fields.
    There are now also dedicated typed fields bindings equivalent to the Go ones as an alternative to new Field({type: "...", ...}):

    All previous dynamic Collection.options.* fields are flattened for consistency with the JSON Web APIs:

    collection.options.query = "..."
    collection.viewQuery = "..."
    collection.options.manageRule = "..."
    collection.manageRule = "..."
    collection.options.onlyVerified = true
    collection.authRule = "verified = true"
    collection.options.onlyVerified = true
    collection.authRule = "verified = true"
    collection.options.allowOAuth2Auth = true
    // note: providers can be set via collection.oauth2.providers collection.oauth2.enabled = true
    collection.options.allowUsernameAuth = true
    collection.passwordAuth = { "enabled": true, identityFields: ["username"], }
    collection.options.allowEmailAuth = true
    collection.passwordAuth = { "enabled": true, identityFields: ["email"], }
    collection.options.requireEmail = true collection.options.exceptEmailDomains = ["example.com"] collection.options.onlyEmailDomains = ["example.com"]
    // you can adjust the system "email" field options in the same way as the regular Collection.fields, e.g.: collection.fields.getByName("email").require = true collection.fields.getByName("email").exceptDomains = ["example.com"] collection.fields.getByName("email").onlyDomains = ["example.com"]
    collection.options.minPasswordLength = 10
    // you can adjust the system "password" field options in the same way as the regular Collection.fields. e.g.: collection.fields.getByName("password").min = 10

    The Collection create/update operations are also simplified and the Dao and CollectionUpsertForm abstractions have been removed.

    $app.save(collection) both validate and persist the collection. Use $app.saveNoValidate(collection) if you want to skip the validations.

    Below is an example CollectionUpsertForm migration:

    const collection = new Collection() const form = new CollectionUpsertForm($app, collection) form.name = "example" form.type = "base" form.listRule = null form.viewRule = "@request.auth.id != ''" form.createRule = "" form.updateRule = "@request.auth.id != ''" form.deleteRule = null form.schema.addField(new SchemaField({ name: "title", type: "text", required: true, options: { max: 10, }, })) form.schema.addField(new SchemaField({ name: "users", type: "relation", options: { maxSelect: 5, collectionId: "ae40239d2bc4477", cascadeDelete: true, }, })) form.submit()
    const collection = new BaseCollection("name") collection.listRule = null collection.viewRule = "@request.auth.id != ''" collection.createRule = "" collection.updateRule = "@request.auth.id != ''" collection.deleteRule = null collection.fields.add(new TextField({ name: "title", required: true, max: 10, })) collection.fields.add(new RelationField({ name: "users", maxSelect: 5, collectionId: "ae40239d2bc4477", cascadeDelete: true, })) $app.save(collection)

    If your existing migrations are all autogenerated, you may find it easier to upgrade by deleting all of them and run with the PocketBase v0.23.0 executable ./pocketbase migrate collections which will create a full snapshot of your latest collections configuration.

    Optionally, after that you can also run ./pocketbase migrate history-sync to remove the deleted migrations from the applied migrations history.

    The migration function accept the transactional app instance as argument instead of dbx.Builder. This allow access not only to the database but also to the app settings, filesystem, mailer, etc.

    migrate((db) => { const dao = new Dao(db); const settings = dao.findSettings() settings.meta.appName = "test" settings.logs.maxDays = 2 dao.saveSettings(settings) })
    migrate((app) => { const settings = app.settings() settings.meta.appName = "test" settings.logs.maxDays = 2 app.save(settings) })

    PocketBase v0.23.0 ships with a new router built on top of the standard Go 1.22 mux enhancements .
    Some of the more notable differences are:

    • Route path parameters must comply with the Go 1.22 mux syntax .
      Or in other words :param must be replaced with {param}. For static path parameter instead of * you have to use special {path...} param.
    • Similar to the new hook handlers, middlewares must call e.next() in order to proceed with the execution chain.
    • To access the auth state in a route you can call e.auth. Similarly, to set a new logged auth state you can simply assign a new auth record to the e.auth field.
    • Because admins are now _superusers auth collection records, e.auth will be set to the authenticated superuser.
      To check whether e.auth is a superuser you can use the e.hasSuperuserAuth() helper (or alternatively check the record method e.auth.isSuperuser() or e.auth.collection().name == "_superusers").
    • $apis.requestInfo(c) has been replaced with e.requestInfo().
      Note also that the requestInfo.data field was renamed to requestInfo.body for consistency with the @request.body.* API rules change.

    Below you can find a short comparison table with the new routing interface:

    routerAdd("GET", "/hello/:name", (c) => { let name = c.pathParam("name") return c.json(200, { "message": "Hello " + name }) }, $apis.requireRecordAuth())
    routerAdd("GET", "/hello/{name}", (e) => { let name = e.request.pathValue("name") return e.json(200, { "message": "Hello " + name }) }, $apis.requireAuth())
    routerAdd("GET", "/*", $apis.staticDirectoryHandler("/path/to/public", false))
    routerAdd("GET", "/{path...}", $apis.static("/path/to/public", false))
    routerUse((next) => { const header = c.request().header.get("Some-Header") if (!header) { throw new BadRequestError("Invalid request") } return (c) => { return next(c) } })
    routerUse((e) => { const header = e.request.header.get("Some-Header") if (!header) { throw new BadRequestError("Invalid request") } return e.next(); })
    // "c" is the echo httpContext const admin = c.get("admin") // admin model const record = c.get("authRecord") // auth record model // you can also read the auth state from the cached request info const info = $apis.requestInfo(c) const admin = info.admin const record = info.authRecord const isAdminAuth = admin != null
    // "e" is the request event const auth = e.auth // auth record model // you can also read the auth state from the cached request info const info = e.requestInfo() const auth = info.auth const isSuperuserAuth = auth?.isSuperuser() // or e.hasSuperuserAuth()
    const id = c.pathParam("id")
    const id = e.request.pathValue("id")
    const search = c.queryParam("search") // or via the cached request object const search = $apis.requestInfo(c).query.search
    const search = e.request.url.query().get("test") // or via the cached request object const search = e.requestInfo().query.search
    const token = c.request().header.get("Some-Header") // or via the cached request object (the header value is always normalized) const token = $apis.requestInfo(c).headers["some_header"]
    const token = e.request.header.get("Some-Header") // or via the cached request object (the header value is always normalized) const token = e.requestInfo().headers["some_header"]
    c.response().header().set("Some-Header", "123")
    e.response.header().set("Some-Header", "123")
    // read/scan the request body fields into a typed object // (note that the echo request body cannot be read twice with "bind") const body = new DynamicModel({ title: "", }) c.bind(body) console.log(body.title) // read the body via the cached request object const body = $apis.requestInfo(c).data console.log(body.title) // read single multipart/form-data field const title = c.formValue("title") // read single multipart/form-data file const doc = c.formFile("document") // convert the multipart/form-data file into Array<$filesystem.File> const doc2 = [$filesystem.fileFromMultipart(doc)]
    // read/scan the request body fields into a typed object // (it safe to be read multiple times) const body = new DynamicModel({ title: "", }) e.bindBody(body) console.log(body.title) // read the body via the cached request object const body = e.requestInfo().body console.log(body.title) // read single multipart/form-data field const title = e.request.formValue("title") // read single multipart/form-data file const doc = e.request.formFile("document") // return the uploaded files as Array<$filesystem.File> const doc2 = e.findUploadedFiles("document")
    // send response with json body c.json(200, {"name": "John"}) // send response with string body c.string(200, "Lorem ipsum...") // send response with html body // (check also the "Rendering templates" section) c.html(200, "<h1>Hello!</h1>") // redirect c.redirect(307, "https://example.com") // send response with no body c.noContent(204)
    // send response with json body e.json(200, {"name": "John"}) // send response with string body e.string(200, "Lorem ipsum...") // send response with html body // (check also the "Rendering templates" section) e.html(200, "<h1>Hello!</h1>") // redirect e.redirect(307, "https://example.com") // send response with no body e.noContent(204)
    $apis.requireRecordAuth([optCollectionNames...])
    $apis.requireAuth([optCollectionNames...])
    $apis.requireAdminAuth()
    // the same as $apis.requireAuth("_superusers") $apis.requireSuperuserAuth()
    $apis.requireAdminAuthOnlyIfAny($app)
    N/A
    $apis.requireAdminOrRecordAuth("users")
    $apis.requireAuth("_superusers", "users")
    $apis.requireAdminOrOwnerAuth(ownerIdParam)
    $apis.requireSuperuserOrOwnerAuth(ownerIdParam)
    $apis.activityLogger(app)
    // No longer needed because it is registered by default for all routes. // If you want to disable the log of successful route execution you can attach the $apis.skipSuccessActivityLog() middleware.
    $apis.recordAuthResponse($app, c, record)
    $apis.recordAuthResponse(e, record)
    $apis.enrichRecord(c, $app.dao(), record, "categories") $apis.enrichRecords(c, $app.dao(), records, "categories")
    $apis.enrichRecord(e, record, "categories") $apis.enrichRecords(e, records, "categories")

    For more details about all new hooks and their event properties, please refer to the on* method docs in the JSVM reference.

    onBeforeBootstrap((e) => { // ... })
    onBootstrap((e) => { // ... e.next() })
    onAfterBootstrap((e) => { // ... })
    onBootstrap((e) => { e.next() // ... })
    onBeforeApiError((e) => { // ... })
    routerUse((e) => { try { e.next() } catch (err) { // ... } })
    onTerminate((e) => { // ... })
    onTerminate((e) => { // ... e.next() })
    onModelBeforeCreate((e) => { // e.model ... })
    // note: onModelCreate is available if you are targeting non-Record/Collection models onRecordCreate((e) => { // e.record ... e.next() })
    onModelAfterCreate((e) => { // e.model ... })
    // note: onModelAfterCreateSuccess is available if you are targeting non-Record/Collection models onRecordAfterCreateSuccess((e) => { // e.record ... e.next() })
    onModelBeforeUpdate((e) => { // e.model ... })
    // note: onModelUpdate is available if you are targeting non-Record/Collection models onRecordUpdate((e) => { // e.record ... e.next() })
    onModelAfterUpdate((e) => { // e.model ... })
    // note: onModelAfterUpdateSuccess is available if you are targeting non-Record/Collection models onRecordAfterUpdateSuccess((e) => { // e.record ... e.next() })
    onModelBeforeDelete((e) => { // e.model ... })
    // note: onModelDelete is available if you are targeting non-Record/Collection models onRecordDelete((e) => { // e.record ... e.next() })
    onModelAfterDelete((e) => { // e.model ... })
    // note: onModelAfterDeleteSuccess is available if you are targeting non-Record/Collection models onRecordAfterDeleteSuccess((e) => { // e.record ... e.next() })
    onMailerBeforeAdminResetPasswordSend((e) => { // ... })
    onMailerRecordPasswordResetSend((e) => { // e.admin -> e.record ... e.next() }, "_superusers")
    onMailerAfterAdminResetPasswordSend((e) => { // ... })
    onMailerRecordPasswordResetSend((e) => { e.next() // e.admin -> e.record ... }, "_superusers")
    onMailerBeforeRecordResetPasswordSend((e) => { // ... })
    onMailerRecordPasswordResetSend((e) => { // ... e.next() })
    onMailerAfterRecordResetPasswordSend((e) => { // ... })
    onMailerRecordPasswordResetSend((e) => { e.next() // ... })
    onMailerBeforeRecordVerificationSend((e) => { // ... })
    onMailerRecordVerificationSend((e) => { // ... e.next() })
    onMailerAfterRecordVerificationSend((e) => { // ... })
    onMailerRecordVerificationSend((e) => { e.next() // ... })
    onMailerBeforeRecordChangeEmailSend((e) => { // ... })
    onMailerRecordEmailChangeSend((e) => { // ... e.next() })
    onMailerAfterRecordChangeEmailSend((e) => { // ... })
    onMailerRecordEmailChangeSend((e) => { e.next() // ... })

    Side-note: if you are using this hook simply for adding/removing record fields you may want to check onRecordEnrich instead

    onRecordsListRequest((e) => { // ... })
    onRecordsListRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })

    Side-note: if you are using this hook simply for adding/removing record fields you may want to check onRecordEnrich instead

    onRecordViewRequest((e) => { // ... })
    onRecordViewRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })

    With v0.23+ the hook fires before the record validations allowing you to adjust "Nonempty" fields before checking the constraint.

    onRecordBeforeCreateRequest((e) => { // ... })
    onRecordCreateRequest((e) => { // uploaded files are simple values as part of the e.record; // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterCreateRequest((e) => { // ... })
    onRecordCreateRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })

    With v0.23+ the hook fires before the record validations allowing you to adjust "Nonempty" fields before checking the constraint.

    onRecordBeforeUpdateRequest((e) => { // ... })
    onRecordUpdateRequest((e) => { // uploaded files are simple values as part of the e.record; // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterUpdateRequest((e) => { // ... })
    onRecordUpdateRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeDeleteRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterDeleteRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordAuthRequest((e) => { // ... })
    onRecordAuthRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordBeforeAuthWithPasswordRequest((e) => { // ... })
    onRecordAuthWithPasswordRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterAuthWithPasswordRequest((e) => { // ... })
    onRecordAuthWithPasswordRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeAuthWithOAuth2Request((e) => { // ... })
    onRecordAuthWithOAuth2Request((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterAuthWithOAuth2Request((e) => { // ... })
    onRecordAuthWithOAuth2Request((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeAuthRefreshRequest((e) => { // ... })
    onRecordAuthRefreshRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterAuthRefreshRequest((e) => { // ... })
    onRecordAuthRefreshRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    External auths are converted to system _externalAuths collection records.
    onRecordListExternalAuthsRequest((e) => { // ... })
    onRecordsListRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_externalAuths")
    External auths are converted to system _externalAuths collection records.
    onRecordBeforeUnlinkExternalAuthRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_externalAuths")
    External auths are converted to system _externalAuths collection records.
    onRecordAfterUnlinkExternalAuthRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_externalAuths")
    onRecordBeforeRequestPasswordResetRequest((e) => { // ... })
    onRecordRequestPasswordResetRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterRequestPasswordResetRequest((e) => { // ... })
    onRecordRequestPasswordResetRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeConfirmPasswordResetRequest((e) => { // ... })
    onRecordConfirmPasswordResetRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterConfirmPasswordResetRequest((e) => { // ... })
    onRecordConfirmPasswordResetRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeRequestVerificationRequest((e) => { // ... })
    onRecordRequestVerificationRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterRequestVerificationRequest((e) => { // ... })
    onRecordRequestVerificationRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeConfirmVerificationRequest((e) => { // ... })
    onRecordConfirmVerificationRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterConfirmVerificationRequest((e) => { // ... })
    onRecordConfirmVerificationRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeRequestEmailChangeRequest((e) => { // ... })
    onRecordRequestEmailChangeRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterRequestEmailChangeRequest((e) => { // ... })
    onRecordRequestEmailChangeRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRecordBeforeConfirmEmailChangeRequest((e) => { // ... })
    onRecordConfirmEmailChangeRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRecordAfterConfirmEmailChangeRequest((e) => { // ... })
    onRecordConfirmEmailChangeRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRealtimeConnectRequest((e) => { // ... })
    onRealtimeConnectRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRealtimeDisconnectRequest((e) => { // ... })
    onRealtimeConnectRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onRealtimeBeforeMessageSend((e) => { // ... })
    onRealtimeMessageSend((e) => { // ... e.next() })
    onRealtimeAfterMessageSend((e) => { // ... })
    onRealtimeMessageSend((e) => { e.next() // ... })
    onRealtimeBeforeSubscribeRequest((e) => { // ... })
    onRealtimeSubscribeRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onRealtimeAfterSubscribeRequest((e) => { // ... })
    onRealtimeSubscribeRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onFileDownloadRequest((e) => { // ... })
    onFileDownloadRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onFileBeforeTokenRequest((e) => { // ... })
    onFileTokenRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onFileAfterTokenRequest((e) => { // ... })
    onFileTokenRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onCollectionsListRequest((e) => { // ... })
    onCollectionsListRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onCollectionViewRequest((e) => { // ... })
    onCollectionViewRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })

    With v0.23+ the hook fires before the collection validations.

    onCollectionBeforeCreateRequest((e) => { // ... })
    onCollectionCreateRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onCollectionAfterCreateRequest((e) => { // ... })
    onCollectionCreateRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })

    With v0.23+ the hook fires before the collection validations allowing you to adjust "Nonempty" fields before checking the constraint.

    onCollectionBeforeUpdateRequest((e) => { // ... })
    onCollectionUpdateRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onCollectionAfterUpdateRequest((e) => { // ... })
    onCollectionUpdateRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onCollectionBeforeDeleteRequest((e) => { // ... })
    onCollectionDeleteRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onCollectionAfterDeleteRequest((e) => { // ... })
    onCollectionDeleteRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onCollectionBeforeImportRequest((e) => { // ... })
    onCollectionsImportRequest((e) => { // "e.collections" is replaced with "e.collectionsData" which is just a map... e.next() })
    onCollectionsAfterImportRequest((e) => { // ... })
    onCollectionsImportRequest((e) => { e.next() // "e.collections" is replaced with "e.collectionsData" which is just a map... })
    onSettingsListRequest((e) => { // ... })
    onSettingsListRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })

    With v0.23+ the hook fires before the settings validations.

    onSettingsBeforeUpdateRequest((e) => { // ... })
    onSettingsUpdateRequest((e) => { // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() })
    onSettingsAfterUpdateRequest((e) => { // ... })
    onSettingsUpdateRequest((e) => { e.next() // "e.httpContext" is no longer available because "e" is the request event itself ... })
    onAdminsListRequest((e) => { // ... })
    onRecordsListRequest((e) => { // e.admins -> e.records // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminViewRequest((e) => { // ... })
    onRecordViewRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminBeforeCreateRequest((e) => { // ... })
    onRecordCreateRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterCreateRequest((e) => { // ... })
    onRecordCreateRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminBeforeUpdateRequest((e) => { // ... })
    onRecordUpdateRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterUpdateRequest((e) => { // ... })
    onRecordUpdateRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminBeforeDeleteRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterDeleteRequest((e) => { // ... })
    onRecordDeleteRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminAuthRequest((e) => { // ... })
    onRecordAuthRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminBeforeAuthWithPasswordRequest((e) => { // ... })
    onRecordAuthWithPasswordRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterAuthWithPasswordRequest((e) => { // ... })
    onRecordAuthWithPasswordRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminBeforeAuthRefreshRequest((e) => { // ... })
    onRecordAuthRefreshRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterAuthRefreshRequest((e) => { // ... })
    onRecordAuthRefreshRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminBeforeRequestPasswordResetRequest((e) => { // ... })
    onRecordRequestPasswordResetRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterRequestPasswordResetRequest((e) => { // ... })
    onRecordRequestPasswordResetRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")
    onAdminBeforeConfirmPasswordResetRequest((e) => { // ... })
    onRecordConfirmPasswordResetRequest((e) => { // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... e.next() }, "_superusers")
    onAdminAfterConfirmPasswordResetRequest((e) => { // ... })
    onRecordConfirmPasswordResetRequest((e) => { e.next() // e.admin -> e.record // "e.httpContext" is no longer available because "e" is the request event itself ... }, "_superusers")

    $http.send() no longer sends a default Content-Type: application/json header with the request.

    const res = $http.send({ url: "https://example.com/data.json", })
    const res = $http.send({ url: "https://example.com/data.json", headers: { "content-type": "application/json"} })