Skip to main content

Overview

pb-ext uses the functional options pattern for server configuration. This provides a flexible, type-safe way to customize server behavior without breaking changes.

Server Options

All server options are defined in core/server/server_options.go and re-exported through the public facade.

Available Options

WithConfig

Provide a custom PocketBase configuration

WithPocketbase

Use an existing PocketBase instance

InDeveloperMode

Enable developer mode (hot reload, verbose logging)

InNormalMode

Enable production mode (optimized, minimal logging)

Option Functions

InDeveloperMode / InNormalMode

The simplest way to configure the server:
cmd/server/main.go
func initApp(devMode bool) {
    var opts []app.Option

    if devMode {
        opts = append(opts, app.InDeveloperMode())
    } else {
        opts = append(opts, app.InNormalMode())
    }

    srv := app.New(opts...)
    // ...
}
Implementation:
core/server/server_options.go
// InDeveloperMode is a shortcut to enable developer mode.
func InDeveloperMode() Option {
    return func(opts *options) {
        opts.developer_mode = true
        log.Println("🔧 Developer mode")
    }
}

// InNormalMode is a shortcut to disable developer mode.
func InNormalMode() Option {
    return func(opts *options) {
        opts.developer_mode = false
        log.Println("🚀 Production mode")
    }
}
Developer mode sets PocketBase’s DefaultDev flag, enabling features like auto-migrations and verbose logging.

WithConfig

Provide a custom PocketBase configuration:
pbConfig := &pocketbase.Config{
    DefaultDev:     true,
    DefaultDataDir: "./custom_pb_data",
}

srv := app.New(app.WithConfig(pbConfig))
Implementation:
core/server/server_options.go
// WithConfig sets the PocketBase configuration to use.
// Using this together with WithPocketbase will panic.
func WithConfig(config *pocketbase.Config) Option {
    return func(opts *options) {
        opts.config = config
    }
}
WithConfig and WithPocketbase cannot be used together. If both are provided, the server will panic with ErrConfigurationConflict.

WithPocketbase

Use an existing PocketBase instance:
pb := pocketbase.New()
// Customize pb here...

srv := app.New(app.WithPocketbase(pb))
Implementation:
core/server/server_options.go
// WithPocketbase sets a fully initialized PocketBase instance to use.
// Cannot be used together with WithConfig; will panic if a config is already set.
func WithPocketbase(pocketbase *pocketbase.PocketBase) Option {
    return func(opts *options) {
        if opts.config != nil {
            pocketbase.Logger().Error(ErrConfigurationConflict.Error())
            panic(ErrConfigurationConflict)
        }
        opts.pocketbase = pocketbase
    }
}
Use WithPocketbase when you need fine-grained control over the PocketBase instance before pb-ext wraps it.

WithMode

Generic mode setter (used internally by InDeveloperMode / InNormalMode):
core/server/server_options.go
// WithMode sets whether developer mode is enabled.
func WithMode(developer_mode bool) Option {
    return func(opts *options) {
        opts.developer_mode = developer_mode
    }
}

Configuration Patterns

Pattern 1: Simple Mode Toggle

The most common pattern - toggle between dev and production:
cmd/server/main.go
func main() {
    devMode := flag.Bool("dev", false, "Run in developer mode")
    flag.Parse()

    initApp(*devMode)
}

func initApp(devMode bool) {
    var opts []app.Option

    if devMode {
        opts = append(opts, app.InDeveloperMode())
    } else {
        opts = append(opts, app.InNormalMode())
    }

    srv := app.New(opts...)
    // ...
}

Pattern 2: Custom PocketBase Config

Provide a custom data directory or other PocketBase settings:
pbConfig := &pocketbase.Config{
    DefaultDev:     true,
    DefaultDataDir: "./custom_pb_data",
}

srv := app.New(app.WithConfig(pbConfig))

Pattern 3: Existing PocketBase Instance

Use an existing PocketBase instance (useful for testing or advanced setups):
pb := pocketbase.New()
pb.Logger().SetLevel(slog.LevelDebug)
// Other PocketBase customizations...

srv := app.New(app.WithPocketbase(pb))

Pattern 4: Custom Port

Set a custom port using command-line arguments:
cmd/server/main.go
func main() {
    devMode := flag.Bool("dev", false, "Run in developer mode")
    port := flag.String("port", "8090", "HTTP port")
    flag.Parse()

    // Inject custom port into os.Args for PocketBase
    os.Args = []string{"app", "serve", fmt.Sprintf("--http=127.0.0.1:%s", *port)}

    initApp(*devMode)
}
PocketBase reads port configuration from os.Args. Set it before calling srv.Start().

Environment Variables

While pb-ext doesn’t enforce specific environment variables, you can integrate them with standard Go patterns:
func main() {
    // Read from environment
    devMode := os.Getenv("DEV_MODE") == "true"
    dataDir := os.Getenv("DATA_DIR")
    if dataDir == "" {
        dataDir = "./pb_data"
    }

    pbConfig := &pocketbase.Config{
        DefaultDev:     devMode,
        DefaultDataDir: dataDir,
    }

    srv := app.New(app.WithConfig(pbConfig))
    // ...
}

Common Environment Variables

VariablePurposeExample
DEV_MODEEnable developer modetrue / false
DATA_DIRPocketBase data directory./pb_data
HTTP_PORTServer port8090
LOG_LEVELLogging leveldebug / info / warn / error

PocketBase Integration

Accessing PocketBase

The underlying PocketBase instance is accessible via srv.App():
srv := app.New(app.InDeveloperMode())

// Access PocketBase directly
pb := srv.App()

// Use PocketBase APIs
pb.OnServe().BindFunc(func(e *core.ServeEvent) error {
    // Custom route
    e.Router.GET("/custom", func(c *core.RequestEvent) error {
        return c.JSON(200, map[string]string{"message": "Hello"})
    })
    return e.Next()
})

PocketBase Configuration

The pocketbase.Config struct supports these fields:
type Config struct {
    DefaultDev     bool   // Enable developer mode
    DefaultDataDir string // Data directory path
    DefaultDebug   bool   // Enable debug mode
}

Hook Registration

Register hooks on the PocketBase instance before starting the server:
srv := app.New(app.InDeveloperMode())

// Register collections
registerCollections(srv.App())

// Register routes
registerRoutes(srv.App())

// Register jobs
registerJobs(srv.App())

// Custom serve hook
srv.App().OnServe().BindFunc(func(e *core.ServeEvent) error {
    app.SetupRecovery(srv.App(), e)
    return e.Next()
})

// Start server (triggers all hooks)
if err := srv.Start(); err != nil {
    log.Fatal(err)
}

Complete Example

Here’s the complete example from cmd/server/main.go:
cmd/server/main.go
package main

import (
    "flag"
    "log"

    app "github.com/magooney-loon/pb-ext/core"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    devMode := flag.Bool("dev", false, "Run in developer mode")
    generateSpecsDir := flag.String("generate-specs-dir", "", "Generate OpenAPI specs")
    validateSpecsDir := flag.String("validate-specs-dir", "", "Validate OpenAPI specs")
    flag.Parse()

    // OpenAPI spec generation mode
    if *generateSpecsDir != "" {
        gen := app.NewSpecGeneratorWithInitializer(func() (*app.APIVersionManager, error) {
            return initVersionedSystem(), nil
        })
        if err := gen.Generate(*generateSpecsDir, ""); err != nil {
            log.Fatal(err)
        }
        return
    }

    // OpenAPI spec validation mode
    if *validateSpecsDir != "" {
        gen := app.NewSpecGeneratorWithInitializer(func() (*app.APIVersionManager, error) {
            return initVersionedSystem(), nil
        })
        if err := gen.Validate(*validateSpecsDir); err != nil {
            log.Fatal(err)
        }
        return
    }

    initApp(*devMode)
}

func initApp(devMode bool) {
    var opts []app.Option

    if devMode {
        opts = append(opts, app.InDeveloperMode())
    } else {
        opts = append(opts, app.InNormalMode())
    }

    // Option 1: Use a custom PocketBase config
    // pbConfig := &pocketbase.Config{
    //     DefaultDev:     true,
    //     DefaultDataDir: "./custom_pb_data",
    // }
    // opts = append(opts, app.WithConfig(pbConfig))

    // Option 2: Use an existing PocketBase instance
    // pb := pocketbase.New()
    // opts = append(opts, app.WithPocketbase(pb))

    // Set custom port programmatically
    // os.Args = []string{"app", "serve", "--http=127.0.0.1:9090"}

    srv := app.New(opts...)

    app.SetupLogging(srv)

    registerCollections(srv.App())
    registerRoutes(srv.App())
    registerJobs(srv.App())

    srv.App().OnServe().BindFunc(func(e *core.ServeEvent) error {
        app.SetupRecovery(srv.App(), e)
        return e.Next()
    })

    if err := srv.Start(); err != nil {
        srv.App().Logger().Error("Fatal application error",
            "error", err,
            "uptime", srv.Stats().StartTime,
            "total_requests", srv.Stats().TotalRequests.Load(),
        )
        log.Fatal(err)
    }
}

Options Internal Structure

The internal options struct:
core/server/server_options.go
type options struct {
    config         *pocketbase.Config
    pocketbase     *pocketbase.PocketBase
    developer_mode bool
}

type Option func(*options)

Configuration Conflict

Attempting to use both WithConfig and WithPocketbase results in a panic:
core/server/server_options.go
var ErrConfigurationConflict = errors.New(
    `WithConfig cannot be used together with WithPocketbase, cause second ` +
    `contains already initialized pocketbase.Config instance. Just pass your ` +
    `config into pocketbase.NewWithConfig func, that's enough.`,
)

Best Practices

1

Use Simple Mode Toggle

Prefer InDeveloperMode() / InNormalMode() for most use cases
2

Only Customize When Needed

Only use WithConfig / WithPocketbase if you need advanced PocketBase customization
3

Environment-Aware Configuration

Read mode and settings from environment variables or flags
4

Register Hooks Before Start

Always register user hooks before calling srv.Start()

Next Steps