PocketBase — Architecture#
Architectural style#
Event-driven Plugin Framework (Microkernel variant)
PocketBase is a monolith in deployment terms (one binary, one SQLite file), but internally it is structured as a microkernel: the core.App interface is the kernel, and every subsystem — including the HTTP server, plugins, and JavaScript runtime — attaches to it by registering event hook handlers. There is no traditional service/repository layer separation; instead, all cross-cutting concerns (validation, file handling, auth, realtime) are wired through a typed, priority-ordered hook chain.
Evidence:
core.BaseAppholds ~60 typedhook.Hook[T]fields — one per lifecycle event (bootstrap, record create/update/delete, HTTP request, mailer send, etc.)apis.Serve()firesapp.OnServe()to let plugins attach routes before the listener startsplugins/jsvmregisters Go-side hooks that evaluate JavaScript when the hook fires — the entire JS extension system is just another set of hook handlers
Component diagram (textual)#
┌──────────────────────────────────────────────────────────────────────────┐
│ examples/base/main.go │
│ (binary entry point) │
│ pocketbase.New() → plugins.Register() → app.Start() │
└───────────────────────────────┬──────────────────────────────────────────┘
│ embeds
┌───────────▼────────────┐
│ pocketbase.PocketBase │ (library facade / CLI wiring)
│ embeds core.App │
│ RootCmd *cobra.Command │
└───────────┬────────────┘
│ delegates to
┌─────────────────────▼──────────────────────┐
│ core.App (interface) │
│ core.BaseApp (implementation) │
│ │
│ • SQLite (data DB + aux DB, dual pools) │
│ • Settings (stored in _params table) │
│ • ~60 hook.Hook[T] fields │
│ • Cron scheduler │
│ • SubscriptionsBroker (SSE) │
│ • Store[string,any] (in-memory KV) │
└──────┬──────────┬──────────────────────────┘
│ │
┌────────────▼──┐ ┌───▼────────────────┐
│ apis/ │ │ plugins/ │
│ HTTP layer │ │ jsvm (Goja JS) │
│ route │ │ migratecmd (CLI) │
│ registration │ │ ghupdate (self) │
└──────┬────────┘ └────────────────────┘
│ uses
┌──────▼────────────────────────────────────────────┐
│ tools/ (utility micro-packages) │
│ hook · router · store · cron · subscriptions │
│ filesystem · mailer · auth · search · security │
│ types · inflector · tokenizer · routine · logger │
└───────────────────────────────────────────────────┘
│ embedded
┌──────▼───────┐
│ ui/dist/ │ (pre-built Svelte Admin UI, go:embed)
└──────────────┘Core components#
core.App / core.BaseApp#
- Package:
github.com/pocketbase/pocketbase/core - Responsibility: The central kernel. Owns all infrastructure resources: two pairs of SQLite connection pools (concurrent + nonconcurrent, for data and aux DBs), in-memory settings, cron scheduler, SSE subscription broker, and the full set of typed lifecycle hooks.
- Key types:
App(interface, ~150 methods),BaseApp(concrete impl),BaseAppConfig,Settings,TxAppInfo - Dependencies:
tools/hook,tools/cron,tools/store,tools/subscriptions,tools/filesystem,tools/mailer,tools/logger,pocketbase/dbx
tools/hook#
- Package:
github.com/pocketbase/pocketbase/tools/hook - Responsibility: The extensibility mechanism. A generic
Hook[T Resolver]holds a priority-sorted list ofHandler[T]structs. Handlers must calle.Next()to continue the chain — exactly like http middleware. Thread-safe viasync.RWMutex. - Key types:
Hook[T],Handler[T],Event(embeddable base),Resolver(interface requiringNext() error) - Dependencies:
tools/security(for ID generation only)
apis (HTTP layer)#
- Package:
github.com/pocketbase/pocketbase/apis - Responsibility: Route registration and HTTP handlers.
NewRouter(app)creates atools/routerinstance, attaches global middleware (activity logger, panic recovery, rate limiting, auth token loading, security headers, body limit), then binds all API sub-groups under/api/. - Key types:
ServeConfig, route group bindings - Dependencies:
core,tools/router,tools/hook,forms,mails
tools/router#
- Package:
github.com/pocketbase/pocketbase/tools/router - Responsibility: A thin wrapper around Go 1.22
net/httpstdlib router (using the new pattern matching with{param}and{path...}wildcards). Exposes a middleware-style hook chain for each route viatools/hook. - Key types:
Router[T],Route[T],Group[T] - Dependencies: stdlib
net/http,tools/hook
plugins/jsvm#
- Package:
github.com/pocketbase/pocketbase/plugins/jsvm - Responsibility: Embeds the Goja JavaScript runtime. Loads scripts from
pb_hooks/andpb_migrations/directories, pre-warms a pool of Goja runtimes, and registers Go-side hook handlers that invoke JS callbacks for every core lifecycle event (record CRUD, HTTP requests, auth flows, etc.). - Key types:
Config, pool ofgoja.Runtimeinstances - Dependencies:
core,tools/hook,github.com/dop251/goja
migrations#
- Package:
github.com/pocketbase/pocketbase/migrations - Responsibility: Built-in system migrations auto-applied during
Bootstrap()viaapp.RunSystemMigrations(). Uses a sequential registry pattern — each migration is a Go function registered with a timestamp key. - Dependencies:
core
Data flow#
HTTP request (record create example)#
net.Listener (TCP)
→ http.Server.Serve()
→ tools/router mux
→ global middleware chain (priority-ordered hooks):
1. panicRecover
2. activityLogger
3. rateLimit
4. loadAuthToken (parses JWT, sets e.Auth on RequestEvent)
5. securityHeaders
6. BodyLimit
→ route handler: apis.recordCreateHandler()
→ fires app.OnRecordCreateRequest hook
→ forms.RecordUpsert.Submit()
→ fires app.OnRecordValidate hook
→ fires app.OnRecordCreate hook
→ fires app.OnRecordCreateExecute hook (writes to SQLite)
→ fires app.OnRecordAfterCreateSuccess hook
→ response JSON written to e.ResponseRealtime (SSE)#
Client HTTP GET /api/realtime
→ RealtimeConnect handler
→ subscriptions.Broker.Connect() → creates SSE client
→ goroutine holds connection open
→ on record change: app fires OnRecordAfterCreateSuccess / UpdateSuccess / DeleteSuccess
→ subscriptionsBroker fans out messages to matching SSE clientsInitialization / Bootstrap#
Sequence:
pocketbase.New()— createsPocketBasewith acobra.Commandandcore.BaseApp, eagerly parses CLI flags (--dir,--dev,--encryptionEnv,--queryTimeout), registers themoderncversion check hook.app.Start()— registersserveandsuperuserCobra subcommands, then callsapp.Execute().app.Execute()— bootstraps the app (unless--help/--version), starts a goroutine for signal handling (SIGTERM/SIGINT), and runs the Cobra command tree.app.Bootstrap()firesapp.OnBootstrap().Trigger(...). The default handler inside the trigger:- Calls
ResetBootstrapState()(closes any existing DB connections) - Creates
pb_data/directory initDataDB()— opens two SQLite connections (WAL mode, concurrent + nonconcurrent builders) usingDefaultDBConnectinitAuxDB()— same for the auxiliary DB (pb_data/auxiliary.dbfor logs)initLogger()— configures structured slog loggerRunSystemMigrations()— applies built-in schema migrationsReloadCachedCollections()— loads collection schemas into in-memory cacheReloadSettings()— loads app settings from_paramstable
- Calls
apis.Serve(app, config)(called fromcmd/serve.go):app.RunAllMigrations()— runs any pending user migrationsNewRouter(app)— registers all API routes- Fires
app.OnServe().Trigger(serveEvent)— plugins attach routes here router.BuildMux()— materializesnet/httpmux- Starts TCP listener and HTTP/HTTPS server
Dependency injection: Manual wiring only. core.App is passed explicitly to every package that needs it (apis, forms, mails, plugins). No DI container. The PocketBase struct embeds core.App directly so callers interact with a single unified interface.
Transaction isolation: app.RunInTransaction(fn) creates a shallow copy of BaseApp with a transaction-aware dbx.Builder, wraps it in TxAppInfo, and passes it to fn. Hook handlers inside the transaction receive this scoped App instance.
Configuration#
| Source | What it configures |
|---|---|
CLI flags (--dir, --dev, --encryptionEnv, --queryTimeout) | Data directory, dev mode, settings encryption, query timeout |
SQLite _params table | All app settings (SMTP, S3, OAuth2 providers, tokens, email templates, rate limits, etc.) loaded via app.ReloadSettings() |
Environment variable (name from --encryptionEnv) | 32-char AES key for optional settings encryption at rest |
Plugin flags (--hooksDir, --hooksPool, --migrationsDir, etc.) | Registered as PersistentFlags on RootCmd by each plugin |
No Viper, no config files. Settings are stored directly in SQLite and reloaded on demand. This is an intentional design decision to keep the binary self-contained.
Key design decisions#
core.Appas the kernel interface (~150 methods): Every subsystem receivescore.Appas its dependency. The large interface is acknowledged as non-ISP-compliant (godoc comment notes it is not meant to be implemented by users), but it provides one-stop access to all infrastructure and enables the transaction isolation pattern. The interface-driven design means all tests can pass acore.BaseAppinstance without mocking.Hook chain as the universal extension point: Rather than separate plugin APIs for routes, middleware, model hooks, and background jobs, PocketBase uses one uniform mechanism —
hook.Hook[T].Trigger()withe.Next()chaining — everywhere. This means JavaScript plugins injsvmuse the same hook API as compiled Go extensions, making the extension model consistent and predictable.Dual SQLite connection pools (concurrent + nonconcurrent):
BaseAppmaintains fourdbx.Builderfields — concurrent and nonconcurrent builders for both data and aux DBs. This is specifically tuned for SQLite’s WAL mode: concurrent reads can use the concurrent pool; writes funnel through the nonconcurrent one to avoid WAL conflicts. This is a deliberate trade-off: not scaling to multiple machines, but squeezing maximum single-node throughput from SQLite.JavaScript extensibility without CGo or subprocess IPC:
plugins/jsvmembeds Goja (pure-Go JS runtime) with a pre-warmed pool ofgoja.Runtimeinstances. This avoids the cold-start cost of a fresh runtime per request while keeping the binary CGo-free. TypeScript type definitions are generated and committed toplugins/jsvm/internal/types/so JS developers get autocomplete without a separate SDK.examples/base/main.goas the canonical production binary: The binary that users download is built from a file namedexamples/base/main.go. This naming convention signals that the PocketBase struct itself is the library, and the binary is just one way to use it. Library users write the same 20-linemain.gopattern, customizing it with their own hooks before callingapp.Start(). The “example” is indistinguishable from production — it is the reference implementation.