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
- Server-Side JavaScript 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 server-side JavaScript package API names.
- Prefer
registerServerJS*/mustRegisterServerJS*andServerJSConfig. RegisterJSVM*/MustRegisterJSVM*andJSVMConfigremain aliases because PocketBase’s upstream JavaScript extension package is namedjsvm.RegisterHooksPlugin*/MustRegisterHooksPlugin*andRegisterServerJS*/MustRegisterServerJS*remain deprecated aliases for compatibility with released PocketBun versions.
- 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.
Server-Side JavaScript Plugin Naming
PocketBase’s upstream JavaScript extension package uses jsvm naming because it runs code in an embedded JavaScript VM. PocketBun runs hooks and JavaScript migrations with Bun, so PocketBun’s preferred package API names use ServerJS:
- preferred:
registerServerJS*,mustRegisterServerJS*,ServerJSConfig - upstream-parity aliases:
RegisterJSVM*,MustRegisterJSVM*,JSVMConfig - released compatibility aliases:
RegisterHooksPlugin*,MustRegisterHooksPlugin*,RegisterServerJS*,MustRegisterServerJS*
All names map to the same server-side JavaScript registration behavior.
Hooks API And Module Loading
PocketBun supports PocketBase-style lowercase server-side JavaScript naming and keeps Go-style aliases where applicable. The uppercase aliases exist only for older PocketBun hooks and migrations, and are deprecated compatibility aliases; new pb_hooks and pb_migrations code should use the lowercase names used by PocketBase JavaScript docs and pb_data/types.d.ts.
- preferred hook object names:
bindFunc,bind,unbind,length,trigger - deprecated alias hook object names:
BindFunc,Bind,Unbind,Length,Trigger - app method style: prefer
$app.onServe();$app.OnServe()remains accepted as a deprecated compatibility alias pb_hooksglobal hook bindings intentionally mirror PocketBase’s upstream JavaScript hooks (so there is no globalonServe(...))
To update older PocketBun hooks and migrations automatically, run:
pocketbun server-js upgrade-source
The command scans ./pb_hooks and ./pb_migrations by default. Use pocketbun server-js upgrade-source --check in CI to fail when deprecated aliases remain, or pass explicit files/directories to limit the rewrite. The fixer rewrites deprecated JavaScript/TypeScript member access and known PocketBun option-object keys such as e.Record.GetString(...), $app.OnServe(), app.RunInTransaction(...), app.CreateBackup(...), app.RecordQuery(...), app.Restart(), record.GetDateTime(...), dateTime.Before(...), requestInfo.HasSuperuserAuth(), cookie.Valid(), command.SetOut(...), message.WriteSSE(...), ctx.Deadline(), form.Validate(), apiErr.RawData(), validationErr.SetMessage(...), { Func, Id, Priority }, new TextField({ Name: "title", Help: "shown" }), and new Command({ Use: "serve", RunE: fn }); updates released package aliases such as RegisterHooksPlugin*, RegisterJSVM*, RegisterServerJS*, RequireGuestOnly, Static, TemplateLangJS, and PascalCase package config keys; and updates old generated collection migrations to use app.forMigrations(). It does not rewrite comments, strings, unrelated application data keys such as HTTP headers or globalThis.secrets.*, or class constructor identifiers such as new ApiError(...), new ValidationError(...), or new RecordUpsertForm(...). Run it with a clean working tree and review the diff before committing.
For pb_hooks module loading:
.pb.tssupports ESM imports from local files and dependencies (node_modules).pb.jssupportsrequire(...)- Deploy artifacts can use
pocketbun hooks buildorregisterServerJSAsync({ bundleHooks: true })to bundle hook dependencies before loading copied hook files
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(...) |
| Server-side JavaScript register | registerServerJS(...) |
registerServerJSAsync(...) |
| Filesystem factories | newFilesystem() |
newFilesystemAsync() |
| Server-side JavaScript 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 Bun’s built-in Bun.Image for image resizing. Output bytes may differ from the PocketBase Go image stack.
- Newly generated thumbnails are intentionally stored as WebP (
Content-Type: image/webp) for every supported source image format. - PocketBase preserves some source formats and falls back to PNG for WebP; PocketBun does not preserve that output-format parity.
WxH,WxHt, andWxHbthumbnails followBun.Imageexact-size resize behavior instead of PocketBase’s center/top/bottom crop positioning.- Thumbnail file keys still follow PocketBase naming, so rely on
Content-Typeinstead of the thumb key extension.
Templates
PocketBun $template helper supports common PocketBase template patterns.
For closer Go text/template parity, install optional go-text-template.
Server-Side JavaScript $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"."
Server-Side JavaScript 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