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 present 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 (the generated go.work, how the build picks up each member’s module), 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' },
package- subdirectory name containing the Go module. Convention is'server'.module- Go module path declared in that subdirectory’sgo.mod. Use thetinycld.org/packages/<slug>namespace so nothing collides with third-party modules.
The generator lists each present feature’s server module in a generated tinycld/server/go.work, so the build resolves your module’s location automatically. On pnpm run packages:generate the wiring stays in sync with the set of present members.
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, and requires core’s module:
module tinycld.org/packages/example
go 1.25.0
require (
github.com/pocketbase/pocketbase v0.38.1
tinycld.org/core v0.0.0
)
Keep the PocketBase version aligned with the app shell’s (tinycld/server/go.mod) - the app’s go.mod is the source of truth, and tinycld/server/go.work lashes every module to the same PB build. Bumping the shell is what drags every package’s go.mod along.
Depend only on what your package actually uses. @tinycld/core’s Go module (tinycld.org/core, nested at tinycld/core/server/) carries the shared dependencies (audit logging, auth helpers, the PocketBase runtime itself) and exposes them under tinycld.org/core/... subpackages you can import. You do not add a replace directive for it - the generated tinycld/server/go.work resolves every member module’s location.
The Register function
Each package server must export func Register(app *pocketbase.PocketBase). The app shell’s generated tinycld/server/package_extensions.go calls it once (via registerPackageExtensions(app)), before the server starts. 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:
- PocketBase record hooks expressible in JS → put a
.pb.jsfile inpb-hooks/and declarehooks: { directory: 'pb-hooks' }in the manifest. - Schema changes → migrations in
pb-migrations/handle those. - Client-only behavior → nothing server-side is required at all.
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 member:
cd ~/code/tinycld/example/server
go test ./...
The member’s Go module is self-contained for testing - it pins the same PocketBase version core uses, so test code hits the same API surface it will in production. Within an assembled workspace, the generated tinycld/server/go.work ties this module, core, and the app together, so a build from tinycld/server/ reflects exactly what the app ships.