Upgrading to v3
v3 takes the script runtime out of HappyView. In v2 a Lua interpreter was compiled into the binary; in v3 a script runs inside an interpreter plugin, and HappyView ships none. An instance with no interpreter installed can neither save nor run a script.
The libraries went the same way. db, http, Record and the rest were the host in v2; in v3 they are library plugins a script reaches through require("happyview.*"), and HappyView ships none of those either.
Everything else about running an instance is where you left it: the database upgrades in place, every environment variable you set for v2 still means the same thing, and the admin API keeps its routes. The breaking changes are concentrated in scripts and plugins.
This guide covers the operator-facing changes. The script contract itself — the removed globals, handle(input, ctx), the codemod’s rewrites and markers — is in Migrating Scripts to v3.
v3 is in alpha. The interpreters and the official libraries are published to the plugin registry, on the alpha channel only, and neither latest nor beta resolves to anything yet. There is also no guided upgrade flow that detects a v2 database and walks you through the codemod; every step below is manual.
Architecture changes
Section titled “Architecture changes”| v2 | v3 |
|---|---|
| Lua compiled into the HappyView binary | Scripts run in an interpreter plugin; none ships with HappyView |
One script language, lua | script_type names whichever language an installed interpreter claims |
| Script bodies checked by the built-in Lua parser | Checked by the interpreter’s own validate export |
Script globals (db, params, Record, now(), …) | handle(input, ctx) plus require("internal.*") / require("happyview.*") |
db, http, Record and the rest built into the host | Library plugins, installed like any other |
Plugin manifests at api_version "1" | api_version "2", declaring capabilities |
Plugins installed from a URL and checked against a sha256 you supply | Installed from a registry of software.release records and verified by CID |
| Instruction limit never fired; no time or memory limit | Instruction limit enforced, plus a request wall clock and a per-run memory ceiling |
1. Install an interpreter plugin
Section titled “1. Install an interpreter plugin”Nothing runs a script until one is installed. Boot says so once:
no interpreter plugin is installed, so no script can be saved or run;install one from the plugins page in the dashboardUntil then, every script surface refuses in its own way:
| Surface | Behaviour with no interpreter |
|---|---|
POST / PATCH /admin/scripts | 400, naming the language |
| XRPC query or procedure backed by a script | 503 ServerMisconfigured, naming the language |
| Record event | Dead-lettered on the first attempt, and the record is still indexed |
| Label event | Dead-lettered on the first attempt, and the label still persists unmodified |
| Job | Fails, with the same message in its error column |
Record and label events are deliberately let through rather than held: an absent interpreter is an operator’s problem, not a reason to stop indexing. The dead-letter rows are the record of what went unprocessed, at Dead Letters in the dashboard.
To see what is installed:
curl "http://127.0.0.1:3000/admin/plugins?type=interpreter" -H "$AUTH"The scripts list also flags each row: runnable is false when no installed interpreter claims its script_type, and the dashboard marks those rows at Integrations > Scripts.
Which interpreter
Section titled “Which interpreter”Three are published, each its own project in the registry:
| Language | script_type | Registry project | Script files |
|---|---|---|---|
| Lua | lua | happyview-lua | .lua |
| JavaScript | javascript | happyview-javascript | .js, .mjs |
| TypeScript | typescript | happyview-typescript | .ts, .mts |
An upgrade needs happyview-lua: every script a v2 instance stored is Lua. The other two are for scripts you write from here on, and all three can be installed at once — an interpreter claims a language id and a set of file extensions, and a second one claiming either is refused at install.
Installing it
Section titled “Installing it”Go to Integrations > Plugins, click Add plugin, pick the project from the registry, choose a channel, review the capabilities it asks for, and install. Installing plugins is the reference; three things about it matter here.
It is the alpha channel. A project publishes up to three — latest, beta and alpha — and these have published only alpha so far. The dashboard picks it for you, because it is the only one the project has; POST /admin/plugins does not, and a registry install naming no channel asks for latest and resolves to nothing. Switch channel moves an install later, once there is somewhere to move it to — see Channels.
There is nothing to checksum. A registry version names its manifest and its module by CID; HappyView fetches both from the registry, refuses either whose bytes don’t match, and stores the CIDs with the install so it checks them again at every boot and reload. A registry install takes no sha256. Verified by CID has the details.
The registry it came from is a boot dependency. While the registry can’t be reached, a plugin installed from it isn’t loaded — which for an interpreter means no script runs. Setting PLUGIN_REGISTRY_URL to off doesn’t change that; it stops HappyView reading the registry, not the stored install’s own fetch. Install from a URL or the file system instead if you need an instance that depends on nothing.
To install without the dashboard, see POST /admin/plugins with registry. To give a fleet its interpreter without anyone clicking, HAPPYVIEW_DEFAULT_PLUGINS installs a list of sources on the first boot that names them — see Default plugins.
If you installed an interpreter from a release URL earlier in the alpha, you needn’t reinstall: the next registry refresh matches a URL install to the official publisher’s project with the same plugin id, and its updates come from the registry from then on. One that matches nothing is marked Unresolved and keeps working — From a URL covers both.
2. Migrate your stored scripts
Section titled “2. Migrate your stored scripts”A script stored before the upgrade stays in the table untouched and fails the first time it reads a removed global. The scripts list reports what each one still references in needs_migration, and the codemod rewrites it: from the editor’s Migrate button, from POST /admin/scripts/{id}/codemod, or from the happyview-codemod CLI.
Migrating Scripts to v3 is the reference for what the rewrite does and what it leaves behind as a -- codemod: marker for you to finish by hand.
Two things that matter for the order you do this in:
- The codemod does not need an interpreter. It is a source rewrite, so you can migrate every stored script before installing one. Editing a script by hand afterwards does need an interpreter, because the save is validated through it.
happyview-codemodis in the published Docker image, at/usr/local/bin/happyview-codemod: rundocker run <image> happyview-codemod …against the sameDATABASE_URL, or usePOST /admin/scripts/{id}/codemodwith{"apply": true}.
Saving is stricter than running: POST / PATCH /admin/scripts refuse a Lua body that still references a removed global, with the names in removed_globals. The codemod’s own apply is the one exception, so a script with markers can be stored once you confirm.
3. Install the libraries your scripts require
Section titled “3. Install the libraries your scripts require”The codemod rewrites a v2 global into a require, and a happyview.* module is a library plugin. A migrated script therefore saves fine and then fails at run time on the first library it names that isn’t installed. Do this before you let traffic reach a migrated script.
The require block the codemod writes at the top of each script is the list of what that script needs. The internal.* modules in it come from the interpreter itself and need nothing installed; each happyview.* module is a project in the registry:
require | Registry project |
|---|---|
happyview.db | happyview-db |
happyview.sql | happyview-sql |
happyview.backlinks | happyview-backlinks |
happyview.record | happyview-record |
happyview.xrpc | happyview-xrpc |
happyview.atproto | happyview-atproto |
happyview.spaces | happyview-spaces |
happyview.linked_repos | happyview-linked-repos |
happyview.jobs | happyview-jobs |
happyview.http | happyview-http |
They install exactly as an interpreter does, from the same registry and the same alpha channel. The registry carries libraries the codemod never writes a require for, happyview-blobs among them; install those if you reach for them in a script you write yourself.
The codemod’s polyfills are libraries too. The Record shim needs happyview-record at the version shipped with v3 — the one exporting lexicon — and says so by name if it meets an older one; the row-shape shim reads through happyview-db, happyview-backlinks or both. Polyfills says which shim a script got and why.
4. Rebuild or replace your plugins
Section titled “4. Rebuild or replace your plugins”v2 loaded plugins whose manifest declared api_version "1". v3 requires "2", and a manifest below it is refused at load with a message naming the plugin type:
auth plugins need an api_version: "2" manifest declaring capabilities(got api_version "1"); republish the plugin with a capabilities-declaring manifestThere is no fallback for any plugin type, auth included. A v2-era manifest also has to declare its capabilities, and the loader cross-checks that declaration against the compiled module’s import section — a plugin that imports something its manifest does not cover is refused before it runs.
The instance still boots. A refused plugin logs Failed to install plugin at boot or Failed to load plugins from database and is simply absent, so anything that depended on it — account linking through an auth plugin, say — stops working until you replace it. Check your boot log after the upgrade.
The official auth plugins — Steam, Xbox, Microsoft and itch.io — are published in the registry alongside the interpreters and libraries, on the same alpha channel. Replace a v2-era install by removing it and adding it from the registry. For a plugin of your own, republish it against the v3 SDK with a capabilities-declaring manifest; Developing Plugins covers what it now has to declare.
5. Script limits now apply
Section titled “5. Script limits now apply”v2 set a one-million-instruction limit and then never enforced it: the instruction counter was installed on the wrong thread, so the coroutine handle ran on never saw it. v3 enforces it, and adds two limits v2 had none of. A script that quietly ran long in v2 can fail in v3.
| Limit | Default | Applies to | Setting |
|---|---|---|---|
| Instructions per run | 1,000,000 | Every kind but jobs | script_instruction_limit, env SCRIPT_INSTRUCTION_LIMIT, range 1,000–1,000,000,000 |
| Wall clock per run | 10 s | Every kind but jobs | script_wall_clock_seconds, env SCRIPT_WALL_CLOCK_SECONDS, range 1–300 |
| Memory per run | 64 MiB | Every kind, jobs included | Not configurable |
Jobs are exempt from the first two — running long is what a job is for, and ctx.job.should_stop() is its stop.
Both settings resolve database → environment → default, so the Instruction limit and Request wall clock fields in System > General take precedence over the environment. A value outside the range is refused on save; an environment value outside it is warned about and the default stands. A change lands in-process and reaches a second instance on the same database within ten seconds.
The wall clock counts only time the script itself runs. Waiting on the host — an HTTP request, a database read — is not charged to it.
A run that spends either budget fails with timeout; one that exhausts its memory fails with memory. An XRPC caller sees {"error": "script_error", "errorType": "timeout"} at 408, or "memory" at 500.
6. Error text where you grep it
Section titled “6. Error text where you grep it”If you have alerting or log queries over script failures, two columns changed shape.
Dead-letter and job error columns. v2 wrote a stage prefix naming where in the host the failure happened — db api:, xrpc api:, script load failed:, missing handle():, create sandbox:. v3 writes the failure’s category instead: syntax:, runtime:, timeout:, memory: or missing_handle:, followed by the interpreter’s own text, or <category> in <file>: … when the run has a file. The old prefixes match nothing.
An unrecognised script_type. v2 validated the field during deserialisation, so an unknown language was a 422 from the request body. v3 accepts any string there and refuses it in the handler with a 400 naming the language, because what is valid now depends on which interpreters are installed.
7. spaceType on the plugin and script surfaces
Section titled “7. spaceType on the plugin and script surfaces”A space’s type NSID is named spaceType everywhere the protocol names it: the plugin SDK’s SpaceInfo and SpacesCreate, and ctx.space.spaceType in a script. It was type on the SDK and type_nsid on ctx.space.
The HTTP surface is unaffected. createSpace and listSpaces accept spaceType and still accept type, and a space in a response still carries type.
8. Library record reads return an envelope
Section titled “8. Library record reads return an envelope”A record read through a library — require("happyview.db")’s get, search and records(…) chains, and require("happyview.backlinks") — answers {uri, did, collection, rkey, cid, indexed_at, record}, with the stored body verbatim under record. v2 answered the body with uri written onto it. cid is null while the row holds a local write’s placeholder, and indexed_at is null until Jetstream has echoed the record.
The public XRPC query routes keep their flat body-plus-uri shape. That is a client contract, and it did not change — a scriptless lexicon query still answers what it answered in v2.
A script the codemod migrated keeps v2’s flat rows, through a shim the codemod inlines; Migrating Scripts to v3 describes it and how to retire it.
9. Building from source
Section titled “9. Building from source”A default build compiles no Lua interpreter at all — mlua sits behind a non-default lua-reference feature. The resulting binary cannot run a script without an interpreter plugin, which is the point.
lua-reference is not a fallback runtime. It exists so the test suite can compare the interpreter plugin against the implementation it replaced; a build with it enabled still routes every script run through the plugin.
Two new environment variables, both optional and additive:
| Variable | Default | Description |
|---|---|---|
PLUGIN_CACHE_DIR | plugin-cache beside the SQLite database, else none | Where compiled plugin modules are kept between restarts. A Postgres instance has no such directory of its own and keeps the cache in memory unless you set this |
HAPPYVIEW_DEFAULT_PLUGINS | (none) | Comma-separated plugin sources to install once, on the first boot that lists them. Listing a source consents to every capability its manifest declares. Installs run in the background and never delay boot |
10. Rolling back
Section titled “10. Rolling back”v3’s own schema change is one irreversible migration: it drops happyview_plugin_dedup_keys, a table nothing read or wrote. Nothing else it adds is destructive.
That is not the same as a return path. A script the codemod rewrote will not run on v2, since the globals it now reaches through require were never there. And if you are skipping v2 releases to get here, their migrations come along too — the spaces work in the 2.x line drops columns outright. Take a database backup before the upgrade.
Checklist
Section titled “Checklist”- Install an interpreter plugin from the registry on the
alphachannel, and confirm boot no longer warns - Run the codemod over every stored script, and finish any
-- codemod:markers by hand - Install a library plugin for every
happyview.*module the rewritten scripts require - Confirm
needs_migrationis empty andrunnableis true for every script - Replace any plugin whose manifest is
api_version"1", and check the boot log for refusals - Review long-running scripts against the instruction limit and the 10-second wall clock
- Update any alerting that matches the old stage prefixes in dead-letter or job
errortext - Rebuild any in-house library plugin against the v3 SDK if it reads records or spaces
Next steps
Section titled “Next steps”- Migrating Scripts to v3: the script contract, the codemod, and every marker it emits
- Plugins: installing and configuring plugins
- Developing Plugins: the SDK, capabilities, and the library surface
- Configuration: the full environment variable reference

