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,AuxDBand variants), model CRUD (Save,Delete,Validate,RunInTransactionand 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.Appaccess 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 insideRunInTransaction). Thepocketbase.PocketBasestruct embedscore.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
TxAppis just anAppwith re-routed DB builders. The trade-off is acknowledged; the design works becauseBaseAppis the one real implementation and the interface exists primarily to ease testing and enable theTxApppattern.
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 middlewarenext()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 callsNext(). - Implementations:
hook.Event(embeddable base struct). Every event type in the system embedshook.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 globalFields map[string]FieldFactoryFuncregistry. - 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
actionFuncparameter is the default action — implementors decide whether and when to call it, enabling before/after logic and short-circuiting. Implemented byFileFieldto 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
actionNamestring discriminator (constants:InterceptorActionCreate,InterceptorActionDelete, etc.) allows oneInterceptmethod 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 inApp.Save().LastSavedPK()provides the stable identity for detecting changes since the last save. - Implementations:
BaseModel(embedded byCollection,Record,Log,ExternalAuth,MFA,AuthOrigin,OTP). Companion optional interfacesDBExporter,PreValidator,PostValidatoradd database serialization and validation hooks. - Design quality: Minimal and correct. The
IsNewstate machine is the interesting part — trackinglastSavedPKrather than a boolean flag means the struct starts as “new,” transitions to “persisted” afterMarkAsNotNew()(called byPostScan), 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]ProviderFactoryFuncregistry. 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,Twitterwith non-standard flows). - Design quality: Wide but cohesive — all methods relate to one abstraction (an OAuth2 client). Follows the same registry pattern as
Fieldtypes.
Mailer#
- Package:
github.com/pocketbase/pocketbase/tools/mailer - File:
tools/mailer/mailer.go - Methods:
Send(message *Message) error - Purpose: Minimal email-sending contract. The
Messagestruct carries all richness (MIME, attachments, BCC, etc.). Two implementations:SmtpClient(direct SMTP) andSendmail(OS sendmail binary). Retrieved viaapp.NewMailClient()which selects the implementation based on current settings. - Implementations:
SmtpClient,Sendmail. - Design quality: Textbook ISP adherence — one method. The companion
SendInterceptoroptional 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.
DriverReaderandDriverWriterextendio.ReadCloser/io.WriteCloserwith blob-specific metadata. Thefilesystem.Systemstruct holds aDriverand 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
*Recordand provide typed getter/setter methods for specific fields while still being usable with theApp.Save/App.DeleteAPIs. TheBaseRecordProxyembedded struct satisfies the interface by embedding*Recordand 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
BaseRecordProxyand 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.Resolveris the primary embed-for-extension interface — every event type in the system embedshook.Eventto satisfy it.blob.DriverReaderembedsio.ReadCloserto compose stdlib contracts.BaseRecordProxyembeds*Recordto 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 onField(SetterFinder,RecordInterceptor) are consumer-defined —Fieldimplementors opt-in. Thecore.Appinterface is unusual: defined by providers but explicitly not for user implementation.stdlib interfaces used:
io.ReadCloser(inblob.DriverReader),io.WriteCloser(inblob.DriverWriter). The rest are PocketBase-specific. Notably absent:fmt.Stringer,sort.Interface,http.Handler(PocketBase uses its owntools/routerabstraction rather than rawhttp.Handler).
Key abstractions#
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.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.core.Field— The schema type system. TheFieldsregistry + 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.core.Model— The persistence contract. TheIsNew()/MarkAsNew()state machine implemented here drives all INSERT vs UPDATE decisions and enables the clone-and-save pattern used in migrations and fixtures.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 infilesystem.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.Fieldsregistry (map[string]FieldFactoryFunc) — register new collection field typestools/auth.Providersregistry (map[string]ProviderFactoryFunc) — register new OAuth2 providers Both use the same pattern: a package-level map, populated withinit()calls or explicit registration inmain.go.
Hook-based plugins (runtime):
- Any code can call
app.On<EventName>().Add(handler)to intercept any lifecycle event. Thehook.Resolverinterface is what makes handlers type-safe — each hook isHook[T Resolver]and the concrete event type carries the payload. This is howplugins/jsvmandplugins/ghupdateattach to the application without modifying core.
Proxy pattern (user-space domain models):
core.RecordProxyallows application code to define typed domain structs that wrap*Record. Combined withapp.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.Driverfor local vs S3 storage (selected by settings at runtime viaapp.NewFilesystem())tools/mailer.Mailerfor SMTP vs sendmail (selected by settings viaapp.NewMailClient())- Both are instantiated fresh per operation (not singletons), which keeps the
Appinterface clean of transport lifecycle management.