PocketBase — Interfaces#

Interface catalog#

App#

  • Package: github.com/pocketbase/pocketbase/core
  • File: core/app.go
  • Methods: ~150 methods covering: lifecycle (Bootstrap, ResetBootstrapState, IsBootstrapped, Restart), DB access (DB, ConcurrentDB, NonconcurrentDB, AuxDB and variants), model CRUD (Save, Delete, Validate, RunInTransaction and context/aux variants), collection queries (FindAllCollections, FindCollectionByNameOrId, ReloadCachedCollections), record queries (FindRecordById, FindRecordsByIds, FindAllRecords), auth/user operations, settings (Settings, ReloadSettings), infrastructure (NewMailClient, NewFilesystem, NewBackupsFilesystem, Store, Cron, SubscriptionsBroker), and ~60 typed hook accessors (OnBootstrap, OnServe, OnRecordCreate, etc.)
  • Purpose: The central kernel contract — gives any component that receives core.App access to every infrastructure primitive and lifecycle hook the application exposes. Serves as the single dependency injection point.
  • Implementations: core.BaseApp (and its thin transaction wrapper used inside RunInTransaction). The pocketbase.PocketBase struct embeds core.App.
  • Design quality: Intentionally violates ISP — the godoc explicitly states it is “not intended to be implemented manually by users.” The large interface is justified for two reasons: (1) it enables a single-dependency API across all packages, and (2) it makes transaction scoping clean — a TxApp is just an App with re-routed DB builders. The trade-off is acknowledged; the design works because BaseApp is the one real implementation and the interface exists primarily to ease testing and enable the TxApp pattern.

Resolver#

  • Package: github.com/pocketbase/pocketbase/tools/hook
  • File: tools/hook/event.go
  • Methods:
    Next() error
    nextFunc() func() error          // unexported
    setNextFunc(f func() error)      // unexported
  • Purpose: The contract that every hook event must satisfy. Calling Next() advances the handler chain — this is exactly the middleware next() pattern but made generic and composable via Go generics (Hook[T Resolver]). The two unexported methods form a package-internal protocol for wiring up the chain; external code only ever calls Next().
  • Implementations: hook.Event (embeddable base struct). Every event type in the system embeds hook.Event, e.g. RecordEvent, RequestEvent, ServeEvent.
  • Design quality: Exceptionally well-segregated. Public surface is one method. The unexported pair is a deliberate leaky-abstraction to keep chain wiring internal — a reasonable trade-off for such a core primitive.

Field#

  • Package: github.com/pocketbase/pocketbase/core
  • File: core/field.go
  • Methods:
    GetId() string
    SetId(id string)
    GetName() string
    SetName(name string)
    GetSystem() bool
    SetSystem(system bool)
    GetHidden() bool
    SetHidden(hidden bool)
    Type() string
    ColumnType(app App) string
    PrepareValue(record *Record, raw any) (any, error)
    ValidateValue(ctx context.Context, app App, record *Record) error
    ValidateSettings(ctx context.Context, app App, collection *Collection) error
  • Purpose: The contract all collection field types must satisfy (text, number, file, relation, select, etc.). Covers identity (GetId/GetName), DB schema (ColumnType), value lifecycle (PrepareValue, ValidateValue), and settings validation (ValidateSettings). Field types are registered in a global Fields map[string]FieldFactoryFunc registry.
  • Implementations: All field structs: TextField, NumberField, FileField, RelationField, SelectField, DateField, JSONField, BoolField, AutodateField, URLField, EmailField, EditorField, GeopointField.
  • Design quality: Well-scoped for a type-registry pattern. Companion optional interfaces extend behavior without bloating the base: MaxBodySizeCalculator, SetterFinder, GetterFinder, DriverValuer, MultiValuer, RecordInterceptor. This follows ISP correctly — a text field doesn’t need to implement file-upload lifecycle hooks.

RecordInterceptor#

  • Package: github.com/pocketbase/pocketbase/core
  • File: core/field.go
  • Methods:
    Intercept(ctx context.Context, app App, record *Record, actionName string, actionFunc func() error) error
  • Purpose: Optional field extension interface allowing a field type to hook into record lifecycle actions (create, update, delete, validate). The actionFunc parameter is the default action — implementors decide whether and when to call it, enabling before/after logic and short-circuiting. Implemented by FileField to manage file upload/delete side-effects.
  • Implementations: FileField (primary known implementation).
  • Design quality: Good middleware-in-miniature pattern. Single-method with enough context to make decisions. The actionName string discriminator (constants: InterceptorActionCreate, InterceptorActionDelete, etc.) allows one Intercept method to handle all lifecycle phases without splitting into many narrow interfaces.

Model#

  • Package: github.com/pocketbase/pocketbase/core
  • File: core/db_model.go
  • Methods:
    TableName() string
    PK() any
    LastSavedPK() any
    IsNew() bool
    MarkAsNew()
    MarkAsNotNew()
  • Purpose: The base persistence contract for every DB-backed entity. IsNew() / MarkAsNew() / MarkAsNotNew() drive the INSERT vs UPDATE decision in App.Save(). LastSavedPK() provides the stable identity for detecting changes since the last save.
  • Implementations: BaseModel (embedded by Collection, Record, Log, ExternalAuth, MFA, AuthOrigin, OTP). Companion optional interfaces DBExporter, PreValidator, PostValidator add database serialization and validation hooks.
  • Design quality: Minimal and correct. The IsNew state machine is the interesting part — tracking lastSavedPK rather than a boolean flag means the struct starts as “new,” transitions to “persisted” after MarkAsNotNew() (called by PostScan), and can be forced back to “new” for cloning.

Client (subscriptions)#

  • Package: github.com/pocketbase/pocketbase/tools/subscriptions
  • File: tools/subscriptions/client.go
  • Methods:
    Id() string
    Channel() chan Message
    Subscriptions(prefixes ...string) map[string]SubscriptionOptions
    Subscribe(subs ...string)
    Unsubscribe(subs ...string)
    HasSubscription(sub string) bool
    Set(key string, value any)
    Unset(key string)
    Get(key string) any
    Discard()
    IsDiscarded() bool
    Send(m Message)
  • Purpose: Represents a single connected realtime (SSE) subscriber. Combines a communication channel (Channel() chan Message), a per-client key-value context store (Get/Set/Unset), and lifecycle management (Discard/IsDiscarded). The subscription topic model supports query-parameter-encoded options for per-subscription request headers and query params.
  • Implementations: DefaultClient (the only built-in implementation; the interface exists to allow test doubles and alternative transports).
  • Design quality: Reasonably sized. The blending of pub/sub state (subscriptions) with request context storage (Get/Set) in one interface is a pragmatic choice for SSE, where headers from the original request need to propagate to subscription filter hooks.

Provider (auth)#

  • Package: github.com/pocketbase/pocketbase/tools/auth
  • File: tools/auth/auth.go
  • Methods: ~20 methods covering: configuration setters/getters (ClientId, ClientSecret, RedirectURL, AuthURL, TokenURL, Scopes, PKCE, DisplayName, UserInfoURL, ExtraTokenParams), flow execution (BuildAuthURL, FetchToken, FetchRawUserInfo, FetchAuthUser), and context management (Context, SetContext).
  • Purpose: The contract for OAuth2/OIDC providers (Google, GitHub, Apple, Twitter, Discord, etc.). Registered in a global Providers map[string]ProviderFactoryFunc registry. Enables runtime provider selection by name and consistent handling across all 20+ supported OAuth2 backends.
  • Implementations: BaseProvider (base struct with default implementations), extended by provider-specific structs that override only what differs (e.g. Apple, Twitter with non-standard flows).
  • Design quality: Wide but cohesive — all methods relate to one abstraction (an OAuth2 client). Follows the same registry pattern as Field types.

Mailer#

  • Package: github.com/pocketbase/pocketbase/tools/mailer
  • File: tools/mailer/mailer.go
  • Methods:
    Send(message *Message) error
  • Purpose: Minimal email-sending contract. The Message struct carries all richness (MIME, attachments, BCC, etc.). Two implementations: SmtpClient (direct SMTP) and Sendmail (OS sendmail binary). Retrieved via app.NewMailClient() which selects the implementation based on current settings.
  • Implementations: SmtpClient, Sendmail.
  • Design quality: Textbook ISP adherence — one method. The companion SendInterceptor optional interface (method: OnSend() *hook.Hook[*SendEvent]) allows implementors to expose a hook for intercepting sends without forcing every mailer to implement it.

Driver (blob storage)#

  • Package: github.com/pocketbase/pocketbase/tools/filesystem/blob
  • File: tools/filesystem/blob/driver.go
  • Methods:
    NormalizeError(err error) error
    Attributes(ctx context.Context, key string) (*Attributes, error)
    ListPaged(ctx context.Context, opts *ListOptions) (*ListPage, error)
    NewRangeReader(ctx context.Context, key string, offset, length int64) (DriverReader, error)
    NewTypedWriter(ctx context.Context, key, contentType string, opts *WriterOptions) (DriverWriter, error)
    Copy(ctx context.Context, dstKey, srcKey string) error
    Delete(ctx context.Context, key string) error
    Close() error
  • Purpose: Storage backend abstraction over local filesystem and S3-compatible object stores. DriverReader and DriverWriter extend io.ReadCloser / io.WriteCloser with blob-specific metadata. The filesystem.System struct holds a Driver and wraps it with higher-level operations (serving, uploading, thumb generation).
  • Implementations: localDriver (local disk), s3Driver (S3-compatible via custom s3blob package).
  • Design quality: Well-designed. Mirrors the portable blob driver pattern from gocloud.dev/blob (which PocketBase forked/simplified to avoid the dependency). Context threading throughout supports cancellation.

RecordProxy#

  • Package: github.com/pocketbase/pocketbase/core
  • File: core/record_proxy.go
  • Methods:
    ProxyRecord() *Record
    SetProxyRecord(record *Record)
  • Purpose: Allows user-defined typed structs to wrap a *Record and provide typed getter/setter methods for specific fields while still being usable with the App.Save/App.Delete APIs. The BaseRecordProxy embedded struct satisfies the interface by embedding *Record and forwarding the two methods.
  • Implementations: BaseRecordProxy (embed this to implement the interface). User-defined proxy types in application code.
  • Design quality: Extremely well-scoped 2-method interface. The embed pattern means users rarely implement it directly — they just embed BaseRecordProxy and add their typed accessors on top.

Interface patterns#

  • Size distribution: Bimodal — most interfaces are small (1–6 methods: Mailer, Resolver, Model, RecordProxy, RecordInterceptor, blob drivers) with one intentional outlier (core.App ~150 methods). Optional capability interfaces (MaxBodySizeCalculator, SetterFinder, GetterFinder, DriverValuer, MultiValuer, PreValidator, PostValidator) are all single-method or 2-method, following ISP strictly.

  • Embedding: hook.Resolver is the primary embed-for-extension interface — every event type in the system embeds hook.Event to satisfy it. blob.DriverReader embeds io.ReadCloser to compose stdlib contracts. BaseRecordProxy embeds *Record to lift all Record methods onto proxy types.

  • Implicit satisfaction: Both provider-side and consumer-side definitions are used. Infrastructure interfaces (Mailer, Driver) are defined by the providing package, satisfied by concrete implementations. Optional extension interfaces on Field (SetterFinder, RecordInterceptor) are consumer-defined — Field implementors opt-in. The core.App interface is unusual: defined by providers but explicitly not for user implementation.

  • stdlib interfaces used: io.ReadCloser (in blob.DriverReader), io.WriteCloser (in blob.DriverWriter). The rest are PocketBase-specific. Notably absent: fmt.Stringer, sort.Interface, http.Handler (PocketBase uses its own tools/router abstraction rather than raw http.Handler).


Key abstractions#

  1. core.App — The microkernel kernel. Every subsystem receives this interface as its only dependency, making it both the DI container and the event bus. Its large size is the architectural centre of gravity: justified for the single-binary, single-database model where there is only ever one real implementation.

  2. hook.Resolver — The universal extension mechanism. Because every hook event embeds this, the entire plugin system (Go hooks, JS hooks, middleware, route handlers) shares one composition model. This is the simplest interface with the largest blast radius.

  3. core.Field — The schema type system. The Fields registry + interface pattern is what makes PocketBase’s schema system extensible — third-party field types can be registered at startup and treated identically to built-in types by the ORM, validator, and API layer.

  4. core.Model — The persistence contract. The IsNew() / MarkAsNew() state machine implemented here drives all INSERT vs UPDATE decisions and enables the clone-and-save pattern used in migrations and fixtures.

  5. tools/filesystem/blob.Driver — The storage abstraction. This is the boundary between PocketBase’s filesystem layer and the outside world (disk vs S3). Isolating it here means all file handling in filesystem.System (serving, resizing, cleanup) is backend-agnostic.


Interface-driven extensibility#

PocketBase’s interface design creates three distinct extension points:

Registry-based plugins (compile-time):

  • core.Fields registry (map[string]FieldFactoryFunc) — register new collection field types
  • tools/auth.Providers registry (map[string]ProviderFactoryFunc) — register new OAuth2 providers Both use the same pattern: a package-level map, populated with init() calls or explicit registration in main.go.

Hook-based plugins (runtime):

  • Any code can call app.On<EventName>().Add(handler) to intercept any lifecycle event. The hook.Resolver interface is what makes handlers type-safe — each hook is Hook[T Resolver] and the concrete event type carries the payload. This is how plugins/jsvm and plugins/ghupdate attach to the application without modifying core.

Proxy pattern (user-space domain models):

  • core.RecordProxy allows application code to define typed domain structs that wrap *Record. Combined with app.Save(myProxy), it provides the ergonomics of an ORM without a separate ORM layer — the underlying PocketBase persistence machinery operates on the proxied record while the application code works with strongly-typed accessors.

Backend abstraction (swappable implementations):

  • tools/filesystem/blob.Driver for local vs S3 storage (selected by settings at runtime via app.NewFilesystem())
  • tools/mailer.Mailer for SMTP vs sendmail (selected by settings via app.NewMailClient())
  • Both are instantiated fresh per operation (not singletons), which keeps the App interface clean of transport lifecycle management.