JS Embed
Ship an extension's or app's JS hooks and migrations inside the binary, instead of pb_hooks and pb_migrations on
disk. Nothing is written to disk and nothing on disk can override them. PocketHost's mothership binary uses it for its
own hooks.
Install · Configure · Use · Integrations · Example
Install
JS Embed is not in the pocketone binary. Add it to your own Go build: embed the folder and pass it to jsvm as an
extension.
//go:embed hooks
var files embed.FS
hooks, _ := fs.Sub(files, "hooks")
jsvm.MustRegister(app, jsvm.Config{
Extensions: []jsvm.Extension{jsembed.Hooks(hooks), jsembed.Migrations(migrations)},
})
The module is github.com/pockethost/pocketone/ext/jsembed.
Configure
There is no pocketone.yaml section, flag or settings screen. You choose what to embed in Go:
jsembed.Hooks(fsys): the.pb.jsfiles at the root offsys. jsvm'sHooksDiris then ignored and hook watching is off.jsembed.Migrations(fsys): the migration files at the root offsys.MigrationsDiris then ignored.
List only the half you embed; the other stays on disk.
Use
- Put your
.pb.jsfiles at the root of the embedded folder, and shared modules in subfolders (for examplelib/). - Load a shared module with
require(__hooks + "/lib/x.js"). Inside that module,require("./y.js")stays in the embedded folder, and a relativerequirefrom a.pb.jshandler resolves under__hooks. - Rebuild the binary to change a hook. There's no watching and no restart on edit.
What doesn't work, on purpose:
..escapes, absolute disk paths andnode_moduleslookups are "module not found".- JS that reads files itself (
$os.readFile(__hooks + ...)) gets "no such file":__hooksis the virtual path/pb_embedded/pb_hooks, never looked up on disk. A real folder at that path makes no difference.
Integrations
- jsvm extensions.
HooksandMigrationsarejsvm.Extensions, so they sit injsvm.Config.Extensionsnext to other extensions such as JS Errors. - Building on it.
jsembed.Rootis the value of__hookswith embedded hooks. Anyfs.FSworks, not onlyembed.FS: a zip, an overlay, or a test'sfstest.MapFS.
Example
example/main.go serves a route from hooks embedded in the binary, with no pb_hooks folder on
disk. Run go run ./example serve from this folder, then curl http://127.0.0.1:8090/hello. The hook is
example/hooks/main.pb.js; it requires example/hooks/lib/greet.js.
// Command example serves its JS hooks from files embedded in the binary: no pb_hooks folder on disk.
//
// go run ./example serve
// curl http://127.0.0.1:8090/hello
package main
import (
"embed"
"io/fs"
"log"
"ganigeorgiev.com/pocketbase/pocketbase"
"ganigeorgiev.com/pocketbase/pocketbase/plugins/jsvm"
"github.com/pockethost/pocketone/ext/devusers"
"github.com/pockethost/pocketone/ext/jsembed"
)
//go:embed hooks
var files embed.FS
func main() {
hooks, err := fs.Sub(files, "hooks")
if err != nil {
log.Fatal(err)
}
app := pocketbase.New()
devusers.Register(app)
jsvm.MustRegister(app, jsvm.Config{Extensions: []jsvm.Extension{jsembed.Hooks(hooks)}})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
How it works
Hooks and Migrations implement jsvm's HookFiles, Require and MigrationFiles extension methods, PocketOne's
small seam in jsvm; with no extensions, jsvm behaves like upstream. The require registry joins and cleans paths as
text (no symlinks, no disk) and serves only paths under __hooks from the FS. It used to live inside jsvm
(HooksFS/MigrationsFS); it moved here on 2026-11-13.
License
MIT
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.