PocketBun Differences From PocketBase
This page tracks user-relevant differences between PocketBase and PocketBun.
Notation used in examples:
erefers to the hook or route event parameter (for example:onBootstrap((e) => { ... })orrouterAdd("GET", "/path", (e) => { ... })).
Quick links:
- PocketBase To PocketBun Migration Checklist
- Runtime And Distribution
- CLI Defaults And Paths
- Hooks Plugin Naming
- Hooks API And Module Loading
- Migration Hook Behavior
- Async API Extensions
- Operational Differences
- Cron Scheduling
- PocketBase Docs Topics That Do Not Apply Directly
- Intentional Omissions
PocketBase To PocketBun Migration Checklist
Use this as a quick migration recipe for an existing PocketBase project.
- Switch executable and update flow.
- Replace
pocketbasecommands withpocketbun. - There is no PocketBase-style binary self-update command; update via package manager.
- Replace
- Create (or convert to) a Bun project.
- Option A: convert your existing project directory:
- initialize package metadata:
bun init - add PocketBun dependency:
bun add pocketbun
- initialize package metadata:
- Option B: scaffold a new PocketBun project and copy your existing data/code:
- create project:
bun create pocketbun my-app - copy your PocketBase
pb_*directories into the new project (pb_data,pb_hooks,pb_migrations, optionalpb_public)
- create project:
- Option A: convert your existing project directory:
- Keep the project layout, but verify startup working directory.
- Keep
pb_data,pb_hooks,pb_migrations, and optionalpb_public. - PocketBun resolves default paths from current working directory (CWD), so start from your project root or pass explicit dirs.
- Keep
- Move hooks as-is, then fix hook-chain calls.
- In handlers that call
e.next(), return/await it:- sync:
return e.next() - async:
const err = await e.next(); if (err) return err; ...
- sync:
- This is especially important for
onBootstrapwhen using async startup paths. - If you see
OnBootstrap hook didn't fail but the app is still not bootstrapped, this is usually the cause. - In
.pb.tshooks, use standardimportfor neighboring files and dependencies when needed.
- In handlers that call
- Keep API clients and route assumptions.
- Existing client SDK usage should continue to work with the same API base paths (
/api/,/_/).
- Existing client SDK usage should continue to work with the same API base paths (
- If you embed PocketBun programmatically, prefer JSVM-compatible naming.
- Prefer
RegisterJSVM*/MustRegisterJSVM*for naming parity with PocketBase JSVM docs. RegisterHooksPlugin*/MustRegisterHooksPlugin*remain aliases.
- Prefer
- Run a migration smoke test before deploying.
- Start:
pocketbun serve --dev - Verify health:
GET /api/health - Verify custom hooks/routes and auth flows you use in production.
- If you have older generated collection/schema migrations, update them to use
app.forMigrations()as described below.
- Start:
- If you used PocketBase’s automatic HTTPS mode, put PocketBun behind a reverse proxy.
- Run PocketBun on HTTP, for example
pocketbun serve --http 127.0.0.1:8090. - Terminate HTTPS in Caddy, NGINX, Traefik, a load balancer, or another reverse proxy.
- Run PocketBun on HTTP, for example
- Review the sections below for details.
- Use this checklist for the quick pass, then check each section in this page only where your app uses that feature.
Runtime And Distribution
PocketBase:
- distributed as a Go binary
- updated via binary release flow
PocketBun:
- distributed as npm/Bun package
- updated via package manager (
bun update pocketbun) - CLI binary name is
pocketbun
CLI Defaults And Paths
PocketBase defaults are resolved relative to executable location. PocketBun resolves defaults from current working directory.
Default paths in PocketBun CLI:
./pb_data./pb_hooksand./pb_migrations(derived frompb_dataunless explicitly set)./pb_public(unless explicitly set)
This prevents accidental writes into node_modules-adjacent paths when used as package dependency.
Hooks Plugin Naming
PocketBase JS extension naming uses jsvm package naming.
PocketBun keeps those names as primary and also provides aliases:
- preferred for parity:
RegisterJSVM*,MustRegisterJSVM* - aliases:
RegisterHooksPlugin*,MustRegisterHooksPlugin*
Both names map to the same plugin registration behavior.
Hooks API And Module Loading
PocketBun supports JSVM-style lowercase naming and keeps Go-style aliases where applicable:
- preferred hook object names:
bindFunc,bind,unbind,length,trigger - alias hook object names:
BindFunc,Bind,Unbind,Length,Trigger - app method style: prefer
$app.onServe();$app.OnServe()is also accepted pb_hooksglobal hook bindings intentionally mirror upstream JSVM (so there is no globalonServe(...))
For pb_hooks module loading:
.pb.tssupports ESM imports from local files and dependencies (node_modules).pb.jssupportsrequire(...)
For code-first BaseApp usage:
- built-in route middlewares are available as package exports (for example
RequireGuestOnly,SkipSuccessActivityLog) - you can bind them directly in
OnServeroutes withe.Router.GET(...).Bind(...)
Migration Hook Behavior
PocketBase generated collection migrations save collections through the normal app save path. That means custom model/collection hooks can run when old migrations are replayed on a fresh database.
PocketBase does not acknowledge this as a bug; the upstream position is that model save hooks and validations are intentionally part of save. PocketBun disagrees for generated schema migrations because historical migrations should not depend on current application/business hooks. This is the same class of replay hazard described by Rails in Using Models in Your Migrations.
PocketBun generated JS collection migrations use app.forMigrations() instead. The returned app view skips user hooks registered after app construction while preserving PocketBun system hooks required for collection persistence, table sync, cache reloads, and view updates.
For older generated collection/schema migrations, update the migration to use a migration app view:
migrate((app) => {
const migrationApp = app.forMigrations()
const collection = migrationApp.findCollectionByNameOrId("posts")
collection.fields.add(new TextField({
name: "slug",
required: false,
}))
return migrationApp.save(collection)
}, (app) => {
const migrationApp = app.forMigrations()
const collection = migrationApp.findCollectionByNameOrId("posts")
collection.fields.removeByName("slug")
return migrationApp.save(collection)
})
For generated collection snapshots, use:
return app.forMigrations().importCollections(snapshot, false)
Migration rule: migrations must be able to run years later with the current app code.
- Use
app.forMigrations()for collection/schema changes. - Use
app.forMigrations().importCollections(snapshot, false)for generated collection snapshots. - Use SQL for record, data, and settings changes. If SQL is not enough, keep the transformation logic inside the migration and work with the persisted data shape.
- Do not use current app behavior from migrations: no normal record/settings
app.save(...), forms, services, or hook-driven helpers.
Async API Extensions
PocketBun keeps sync-compatible APIs but adds async alternatives for I/O-heavy paths.
| Area | PocketBase-compatible sync API | PocketBun async extension |
|---|---|---|
| Archive helpers | Create, Extract |
CreateAsync, ExtractAsync |
| App bootstrap/serve | app.bootstrap(), serve(...) |
app.bootstrapAsync(), serveAsync(...) |
| Migration helper | migrate(...) |
migrateAsync(...) |
| Hooks plugin register | RegisterJSVM(...) |
RegisterJSVMAsync(...) |
| Filesystem factories | NewFilesystem() |
NewFilesystemAsync() |
| JSVM helpers | $http.send(...), $os.readFile(...) |
$http.sendAsync(...), $os.readFileAsync(...) |
Operational Differences
HTTPS
PocketBase can run a public HTTPS server directly with automatic Let’s Encrypt certificates:
pocketbase serve example.compocketbase serve --https 0.0.0.0:443
PocketBun does not include PocketBase’s built-in automatic HTTPS/Let’s Encrypt server mode. The equivalent PocketBun deployment pattern is to run PocketBun over HTTP and terminate HTTPS in a reverse proxy such as Caddy, NGINX, Traefik, a platform load balancer, or a CDN edge.
Recommended PocketBun backend command:
pocketbun serve --http 127.0.0.1:8090
Minimal Caddy example:
example.com {
reverse_proxy 127.0.0.1:8090
}
The pocketbun serve domain arguments, the --https flag, and programmatic ServeConfig.httpsAddr / ServeConfig.certificateDomains settings are intentionally unsupported and return an explanatory error instead of starting a server.
Activity logs
PocketBun persists activity logs through a background worker to reduce main-thread blocking.
Cron Scheduling
PocketBun app cron scheduling uses Bun’s native Bun.cron(...) scheduler and interprets cron expressions in UTC.
- the
$app.cron().setInterval(...)and$app.cron().setTimezone(...)APIs are not available in PocketBun - programmatic cron setup is expression-based; pass the cron string directly to
cronAdd(...)oradd(...) - cron expression validation follows Bun’s parser, so PocketBun accepts a wider grammar than PocketBase, including named months/weekdays and Sunday as
7 - the Admin UI cron management pages do not rely on per-job timezone settings and assume UTC for built-in backup scheduling
- if your hook code calls
setInterval(...)orsetTimezone(...), remove those calls; in-process cron expressions are interpreted in UTC regardless of the server’s local timezone
Thumbnails
PocketBun uses Sharp for image resizing. Output bytes may differ from PocketBase Go image stack.
- BMP thumbnails are emitted as PNG (Sharp limitation).
Templates
PocketBun $template helper supports common PocketBase template patterns.
For closer Go text/template parity, install optional go-text-template.
JSVM $filepath
PocketBun exposes the same $filepath method names as PocketBase, but it does not fully match Go path/filepath edge cases.
glob(...)andmatch(...)are backed by Bun’s glob engine. Common PocketBase patterns work, and PocketBun also supports Bun-specific patterns such as**even though they are outside Go’s documentedfilepath.Matchsyntax.walk(...)andwalkDir(...)behave like real filesystem traversals and keep lexical depth-first ordering, but the surrounding path helpers follow Bun/Node path semantics in some edge cases.base(...),split(...),splitList(...),join(...), andrel(...)have edge-case differences because they follow Bun/Node path helper behavior.- In particular,
splitList(...)is not Go-compatible; it splits on the path separator instead of the OS path-list separator. - Examples of edge-case differences:
base("")andbase("/")differ from Gofilepath.Base(...)split("foo")yields[".", "foo"]instead of["", "foo"]join()yields"."instead of""rel(path, path)may yield""instead of"."
JSVM RequestEvent request/response surface
For custom routes, e below means the route event parameter passed to routerAdd(..., (e) => { ... }).
PocketBun supports the common PocketBase custom-route access patterns:
e.response.header().set(...)e.request.pathValue(...)ande.request.setPathValue(...)e.request.url.pathe.request.url.query().get(...)e.request.header.get(...)
Incompatibilities in this area:
- Go
http.Requestform helpers are not implemented one.request(formFile,parseForm,parseMultipartForm,formValue,postFormValue).- use
e.findUploadedFiles(...),e.bindBody(...), ore.requestInfo().bodyinstead.
- use
- Go
http.ResponseWriterwrite primitives are not implemented (e.response.write(...),e.response.writeHeader(...)).- use route event helpers (
e.json,e.string,e.html,e.xml,e.blob,e.noContent,e.redirect).
- use route event helpers (
SQL placeholders and dbx rewriting
PocketBun supports dbx-style query marker rewriting for SQLite helpers. Logged placeholder formats can differ while query behavior is compatible.
Dev SQL logging format
In --dev mode, PocketBun prints SQL logs using a Bun-native format based on
the executed rewritten SQL ([X.XXms] <sql>). The exact formatting may differ
from PocketBase and is informational only.
Windows behavior
HooksWatchrestart behavior has no effect on Windows.- filesystem/process timing can differ from Unix-like systems.
PocketBase Docs Topics That Do Not Apply Directly
These upstream topics are either intentionally excluded or need reinterpretation for PocketBun:
- all
go-*extension docs pages (PocketBun is JS/TS extension-first) - binary self-update workflow for PocketBase executable
- built-in
serve [domain]automatic HTTPS instructions; use a reverse proxy for TLS termination instead - operational assumptions tied to standalone Go binary path semantics
- some upstream docs response examples may use slightly different sample keys than runtime output (for example health sample
statusvs runtimecode)
These are not bugs in PocketBun docs; they are product-level differences.
Intentional Omissions
Intentionally not provided in PocketBun:
- PocketBase binary self-update command/plugin workflow
- Go extension workflow as first-class user path
Deferred until demand:
- Dart SDK-specific docs