Server

A package can extend the app shell’s Go server - add custom API endpoints, register PocketBase record hooks in Go, run background tickers, or bind IMAP/SMTP services. The app shell loads each linked package’s server module at startup via a single generated entry point. Most packages do not need this: if all your server-side logic fits in PocketBase JS hooks, stick with pb-hooks/ instead.

For the Go module plumbing (replace directives, go.mod wiring, how the build picks up sibling modules), see Go server integration. This page covers the layout a package author owns.

Declaring a server module

Add a server field to the manifest with two strings:

server: { package: 'server', module: 'tinycld.org/packages/example' },

The generator adds matching require and replace directives to the app shell’s server/go.mod pointing at the sibling’s real path. On npm run packages:generate the wiring stays in sync with tinycld.packages.ts.

Directory layout

A minimal server package looks like this:

example/
    server/
        go.mod
        go.sum
        register.go
        endpoints.go

go.mod declares the module path you put in the manifest:

module tinycld.org/packages/example

go 1.23

require (
    github.com/pocketbase/pocketbase v0.24.0
)

Depend only on what your package actually uses. The app shell’s server bundles @tinycld/core’s Go module (tinycld.org/core), which carries the shared dependencies (audit logging, auth helpers, the PocketBase runtime itself) and exposes them under tinycld.org/core/... subpackages you can import.

The Register function

Each package server must export func Register(app *pocketbase.PocketBase). The app shell’s generated server/package_extensions.go calls it once, before app.Start(). This is where you bind endpoints, register hooks, and kick off any long-lived goroutines:

package example

import (
    "net/http"

    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
    "tinycld.org/core/audit"
)

func Register(app *pocketbase.PocketBase) {
    audit.RegisterCollection(app, "example_items", &audit.CollectionConfig{
        ExtractLabel: audit.LabelFromField("title"),
    })

    app.OnServe().BindFunc(func(e *core.ServeEvent) error {
        e.Router.GET("/api/example/ping", func(c *core.RequestEvent) error {
            return c.JSON(http.StatusOK, map[string]string{"ok": "pong"})
        })
        return e.Next()
    })
}

Register runs whether or not the package has any migrations or collections - it’s wired by manifest alone. If you need access to app state across requests, hold it in package-level variables (as @tinycld/mail does with its settings cache) or attach it to a struct you construct inside Register.

When you do not need server code

Skip the server field entirely if your package only needs:

Go server extensions exist for the cases where JS hooks aren’t enough: network servers (IMAP, SMTP, WebRTC signalling), background workers, HTTP endpoints that need streaming or binary handling, or logic that has to run outside a record event.

Testing server code

Standard Go test files (*_test.go) live next to the package source inside server/. Run them from inside the sibling:

cd ../example/server
go test ./...

The sibling’s Go module is self-contained for testing - it does not need the app shell checked out as long as its go.mod pins the right PocketBase version. When linked into the app shell, the replace directive points tinycld.org/packages/example at this same directory.