SQLite + php-wasm Migration Notes¶
This document records the repo-local changes made to run Moodle in WebAssembly PHP with an in-memory SQLite database (MEMFS-backed file) instead of the previous PGlite/PDO-PGlite path.
Upstream SQLite support
Moodle is tracking native SQLite support in MDL-88218. This project uses an experimental PDO driver patch that restores and maintains SQLite compatibility independently.
Runtime update (March 2025): The PHP runtime was migrated from
seanmorris/php-wasm(v0.0.9-alpha-32) to WordPress Playground's@php-wasm/web+@php-wasm/universal(v3.1.11). This replaced 14 vendored packages and a manual extension-loading pipeline with two npm packages. The compatibility wrapper insrc/runtime/php-compat.jsmaps the WP Playground API to the interface expected by the existing codebase. All PHP extensions (including previously-missingcurl,gd,fileinfo,sodium) are now built into the WASM binary.In-memory refactor (March 2026): The runtime is now explicitly ephemeral. The SQLite database file lives in Emscripten MEMFS (JavaScript heap) with no durable storage. SQLite pragmas are tuned for in-memory operation (
journal_mode=MEMORY,synchronous=OFF,temp_store=MEMORY,cache_size=-8000,locking_mode=EXCLUSIVE). Moodle caching is fully enabled (file-based caches write to MEMFS). Debug mode defaults to disabled for performance, but the initial level can be seeded at boot and later changed from Moodle administration. All state is lost when the browser tab closes.
It is intentionally pragmatic: it lists what was changed, why it was changed, where the change lives, and which caveats still exist in the prototype.
Goal¶
Keep the existing Moodle playground architecture and wasm persistence model, but replace the PGlite-backed database path with Moodle's deprecated SQLite PDO driver.
Constraints followed during the migration:
- keep DB persistence in the writable wasm filesystem
- keep changes localized
- allow local Moodle core patching for the prototype
- avoid redesigning the app
Resulting runtime model¶
- Moodle core is extracted from a prebuilt ZIP bundle into writable MEMFS at
/www/moodle - mutable state lives under
/persist moodledatalives at/persist/moodledata- the SQLite database file lives at
/persist/moodledata/moodle_<scope>_<runtime>.sq3.php config.php, bootstrap helpers, and a few patched PHP files are written into MEMFS at boot
Main migration changes¶
1. Replaced the PGlite runtime path with SQLite¶
What changed:
- removed the active runtime dependency on
@electric-sql/pglite - added
php-wasm-sqlite - registered
sqliteas a browser-side shared library
Files:
package.jsonpackage-lock.jsonscripts/sync-browser-deps.mjssrc/runtime/runtime-registry.jsplayground.config.jsonsrc/runtime/php-loader.js
Current browser-side shared libraries:
domiconvintllibxmlsimplexmlxmlzlibzipmbstringopensslpharsqlite
2. Switched generated Moodle config to SQLite PDO¶
What changed:
config.phpis generated with:$CFG->dbtype = 'sqlite3'$CFG->dblibrary = 'pdo'$CFG->dboptions['file'] = '/persist/moodledata/...sq3.php'- config defaults were added to reduce noise during early bootstrap and first render
rememberusernameis disabled by default to avoid cookie encryption paths during first boot
Files:
src/runtime/config-template.jslib/config-template.js
Relevant defaults seeded during this work:
navcourselimitenablecompletionfrontpagefrontpageloggedinfrontpagecourselimitguestloginbuttonrememberusernameauth_instructionsmaintenance_enabled
3. Restored Moodle's deprecated SQLite PDO driver and missing historical files¶
Restored patch files:
patches/shared/lib/dml/sqlite3_pdo_moodle_database.phppatches/shared/lib/ddl/sqlite_sql_generator.php
Additional historical compatibility patches needed by Moodle 5.0 in this prototype:
patches/shared/lib/classes/encryption.php
Patch copier:
scripts/patch-moodle-source.sh
Patch layout:
patches/shared/is the preferred shared patch rootpatches/moodle/is a legacy fallbackpatches/<branch>/contains optional branch-specific overrides copied relative to the source root
For shared patches, the copier handles legacy-root vs public/ source trees automatically.
Branch-specific overrides should mirror the actual source-root path they target, including
public/ when required.
Why these extra files were needed:
sqlite_sql_generator.php- the old generator signature did not match current
sql_generator - the current prototype needs a temp-table compatible implementation
encryption.php- current Moodle assumes
sodiumis always present - this wasm runtime does not currently ship
sodium - a local OpenSSL fallback was restored so login/session-related encryption can work in the prototype
- runtime bootstrap also downgrades the
admin/environment.xmlsodium requirement to optional so upgrades can proceed
Runtime bootstrap changes¶
The browser bootstrap now does more than just write config.php.
Main file:
src/runtime/bootstrap.js
What it does now:
- extracts the Moodle ZIP bundle into writable MEMFS
- writes runtime files such as:
/www/moodle/config.php/www/moodle/__install_database.php/www/moodle/__config_normalizer.php- SQLite probe scripts
- patches a few Moodle PHP sources in-place at runtime
- loads a pre-built install snapshot (
assets/moodle/snapshot/install.sq3) to skip CLI install - falls back to staged CLI-like provisioning if the snapshot is unavailable
- normalizes persisted config values after install
Important runtime-local overrides added during debugging:
- cache config warnings are suppressed when
CACHE_DISABLE_ALLis active - the deprecated SQLite driver is patched in-place to handle current PHP/Moodle behavior
lib/classes/encryption.phpis patched in-place to add the OpenSSL fallbackadmin/environment.xmlis patched in-place to avoid blocking upgrades on the missingsodiumextension- plugin settings files with brittle
$ADMIN->locate(...)assumptions are patched so install does not abort - install/finalize logic hydrates
$CFGfrom database-backed config during the special bootstrap path
Service Worker and routing fixes¶
Main files:
sw.jssrc/remote/main.jssrc/shared/storage.jsphp-worker.js
What changed:
- the scope was made stable (
main) instead of random UUIDs by default $CFG->wwwrootis built from the actual app base URL, not from the scoped runtime path- scoped redirects preserve query strings
- shell/remote navigation was hardened to avoid cross-origin-style access errors on iframe location inspection
- the remote host now tries to recover from the first-render iframe stall that sometimes leaves the inner Moodle document in
loadingwith an empty body
Known symptom this was addressing:
- the iframe would navigate to a valid Moodle URL such as
/login/index.phpor/my/ - the inner document title changed correctly
- but the body stayed empty and the user saw a white iframe
This area is improved but still the most fragile part of the prototype.
php-cgi bridge fixes¶
Main file:
vendor/php-cgi-wasm/PhpCgiBase.js
What changed:
- incoming HTTP headers are now exported into CGI environment variables
- this includes
HTTP_USER_AGENT,HTTP_ACCEPT_LANGUAGE, and similar headers CONTENT_TYPEandCONTENT_LENGTHare exported correctly too
This was needed because Moodle reads normal CGI server variables and emitted warnings when they were missing.
Bundle loading fix¶
Main file:
lib/moodle-loader.js
Problem:
- the Moodle bundle is large
- the old
fetchWithProgress()implementation kept all chunks and then allocated a second full buffer - that doubled peak memory and could fail with
RangeError: Array buffer allocation failed
Fix:
- when
content-lengthis known, the loader now preallocates a singleUint8Arrayand writes chunks into it directly
Current patch inventory¶
These files contain the long-lived Moodle-side prototype patches:
patches/shared/lib/dml/sqlite3_pdo_moodle_database.phppatches/shared/lib/ddl/sqlite_sql_generator.phppatches/shared/lib/classes/encryption.php
These files contain runtime-only overrides and bootstrap workarounds:
src/runtime/bootstrap.jssrc/runtime/config-template.jslib/config-template.jssrc/remote/main.jssw.jsvendor/php-cgi-wasm/PhpCgiBase.jslib/moodle-loader.js
Required extension status¶
Built into PHP or already satisfied in the runtime path:
ctypedomiconvintljsonmbstringpcresimplexmlsplxmlzippdopdo_sqlitesqlite3openssl
Still missing as shipped wasm shared libraries in this repo:
curlgdfileinfosodium
Prototype workaround currently in place:
sodium- worked around by patching
core\\encryptionto use OpenSSL fallback
Not yet solved at the wasm build level:
curlgdfileinfo
Known caveats¶
- first render inside the nested iframe can still be less reliable than a normal browser-backed PHP stack
- the project currently relies on both:
- source-level Moodle patching at bundle build time
- runtime patching during bootstrap
- some Moodle pages still assume extensions or environment details that are not present in stock
php-wasm - full parity with a normal Moodle PHP environment is not yet claimed
Practical verification checklist¶
Syntax checks:
node --check src/runtime/bootstrap.js
node --check src/runtime/config-template.js
node --check lib/config-template.js
node --check src/remote/main.js
node --check sw.js
node --check lib/moodle-loader.js
php -l patches/shared/lib/classes/encryption.php
php -l patches/shared/lib/dml/sqlite3_pdo_moodle_database.php
php -l patches/shared/lib/ddl/sqlite_sql_generator.php
Bundle patching:
./scripts/patch-moodle-source.sh /path/to/extracted/moodle
Manual browser checks:
- first boot install path
- reload with persisted DB
- login page render
- navigation to
/my/ - theme CSS and JS asset loading
- no fatal on missing
HTTP_USER_AGENT - no fatal on missing
sodium
Why this file exists¶
This repo accumulated several small but necessary prototype fixes while moving Moodle from PGlite to SQLite-on-wasm. They are easy to lose track of when only looking at the final code. This file is the maintained checklist of those changes and should be updated when any of the bootstrap patches, Moodle core patches, or runtime assumptions change.