Go server extensions

A package that needs to run Go code on the server - IMAP/SMTP, custom HTTP endpoints, long-lived workers, anything that doesn’t fit in PocketBase’s JS hooks - ships a server/ subdirectory with its own Go module. The generator wires it into a generated go.work and into the generated tinycld/server/package_extensions.go entry point. For the contract the package itself implements (the Register(app) function, the module layout, testing), see Server. This page covers what the app shell does around that.

When to use Go

Most packages don’t need a Go server. If you can express your logic as:

Reach for Go only when you need:

Core’s Go module

@tinycld/core is a nested member inside the tinycld repo (at tinycld/core/), and its Go side is the module tinycld.org/core at tinycld/core/server/, exporting coreserver (the registration orchestrator) plus subsystems like notify, push, mailer, audit, textextract, thumbnails. The app server (tinycld/server/main.go) is the module tinycld.org/app and consumes core via a hand-written replace directive in tinycld/server/go.mod:

require tinycld.org/core v0.0.0

replace tinycld.org/core => ../core/server

That path is relative to tinycld/server/ and points at the nested core’s server/ subdirectory at ~/code/tinycld/tinycld/core/server/.

Declaring a package module

Two fields in a feature’s manifest:

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

package is the subdirectory name, by convention 'server'. module is the Go module path declared in that subdirectory’s go.mod - use the tinycld.org/packages/<slug> namespace to keep module paths out of collisions. A feature’s go.mod requires tinycld.org/core v0.0.0; it does not need its own replace directive, because the generated go.work resolves every module’s location.

What the generator writes

On each pnpm run packages:generate (and on the workspace-root pnpm install), for every present feature with a server field, the generator:

  1. Regenerates tinycld/server/package_extensions.go, a small Go file whose registerPackageExtensions(app) calls each package’s Register(app). The app shell’s main.go invokes it (RegisterExtras: registerPackageExtensions) so every present package gets a chance to wire in hooks, endpoints, and workers before the server starts.

    // Code generated by tinycld/scripts/generate.ts. DO NOT EDIT.
    package main
    
    import (
        "github.com/pocketbase/pocketbase"
        example "tinycld.org/packages/example"
    )
    
    func registerPackageExtensions(app *pocketbase.PocketBase) {
        example.Register(app)
    }
  2. Writes tinycld/server/go.work, a Go workspace file listing the app, core, and each feature server module by its on-disk path (resolved through the workspace node_modules/@tinycld/* symlinks):

    go 1.25.0
    
    use (
        .
        ../../node_modules/@tinycld/core/server
        ../../node_modules/@tinycld/example/server
    )

    The go.work file is written only when at least one present feature ships a server; it’s removed when none do. Because module locations come from go.work, no per-package replace directives are appended to go.mod - the file stays hand-authored.

  3. Writes tinycld/server/bundled-packages.json, the seed manifest core’s Go server uses to hydrate its pkg_registry collection at boot.

Testing the Go side

A feature’s Go module is self-contained - it pins the same PocketBase version core uses, so test code hits the same API surface it will in production:

cd ~/code/tinycld/example/server
go test ./...

Within an assembled workspace, the generated go.work ties the feature module, core, and the app together, so a build from tinycld/server/ reflects exactly what the app ships. Core also has its own Go tests under tinycld/core/server/**/_test.go — run those from tinycld/core/server/ with go test ./....

For the package-side concerns (the Register function signature, the directory layout, what to import), see Server.