How it works
The pieces
In words, top to bottom:
- The player's device. The game runs with the
RWK_*plugins added to itsjs/plugins.js. A Service Worker (rwk-sw.js) answers requests for pictures, sounds, data and scripts from Cache Storage, and for the page itself when there is no connection. Saves live in the browser first (localforage on MZ,localStorageon MV), andRWK_SaveSyncmirrors them to the cloud. - Cloudflare Access. It sits in front of the site and lets in only the emails you invited. It adds a signed login (a JWT) to each request.
- One Worker.
index.jsroutes the request.auth.jschecks the login's signature and reads the email.saves.jsis the save API and the save history.players.jscounts the emails that saved.settings.jsreads the settings from R2, andtoolkit.jsserves the toolkit's part ofjs/plugins.js. - Storage. Static Assets hold the game files under 25 MiB. One R2 bucket holds the larger files, the saves and their history, the settings and the list of players (details in Storage).
The same Worker serves every game you deploy; each game has its own site name and its own folders in R2.
What a deploy does
In words: the CLI analyzes the game folder and never writes to it. It builds a temporary copy (hard links, plus the plugins and rwk-sw.js), then sends three things to Cloudflare: the Worker with the other files as static assets, the files over 25 MiB to R2, and the settings as a JSON file in R2. The temporary copy is deleted at the end. The build lists each step.
First load and save sync
In words: the first request is stopped by Access, which asks the player to sign in and sets a cookie. After that the Worker serves the page and game files, and reads js/plugins.js with the settings from R2 merged in (Settings per player); the Service Worker starts keeping files for the next launch. At boot, before the engine reads a save, the plugin lists the cloud saves and keeps the newer of each local / cloud pair, asking when both changed. A save then uploads about a second after it is written, with If-Match; the Worker keeps the old copy in the history and answers with a new ETag, or 412 when another device changed it first (Save sync).
These pictures are drawn by site/diagrams/build.mjs (run node site/diagrams/build.mjs, no dependencies); edit that file when the architecture changes, and commit the SVGs it writes to site/public/img/.
Files
run.ps1 ─► cli/ Node CLI: analyzes the game, builds a temporary web build, drives wrangler
runtime/ Plugins injected into the web build (appended to the build's js/plugins.js)
RWK_Core config, device id, toasts, dialogs, ⚙ menu, web build label
RWK_Language ⚙ → Language: points data/ requests (XMLHttpRequest) at data-<code>/ files, file by file
RWK_Translate live translation, off by default: messages wait in Window_Message.canStart for /api/translate,
control codes kept; item / skill names in $data*; a full Vietnamese font
RWK_SaveSync save sync (MZ: localforage, MV: localStorage)
RWK_MobileControls on-screen buttons, driven through Input._currentState
RWK_SafeSaves quick save / load and autosave in slots of their own, ☁ sync state, storage.persist()
RWK_Speed fast forward: the scene updates 2–4 times per frame
RWK_Message message modes: tap to finish, Auto, Skip (Window_Message)
RWK_Backlog message backlog: last messages kept in memory, shown in a panel (Window_Message.startMessage)
RWK_Dash always dash switch (ConfigManager.alwaysDash)
RWK_Remap key and gamepad remapping: edits Input.keyMapper / Input.gamepadMapper, choices in localStorage
RWK_SaveTools save export / import / delete (desktop save file format)
RWK_SaveHistory older saves from the cloud: list and restore
RWK_ItemBatch use several items at once (MZ / MV adapter for the common event queue)
RWK_Cheats cheats, off by default: party, gold and items, win a battle, switches and variables, movement
(god mode: Game_Actor.setHp; walk through walls: Game_Player.isDebugThrough)
RWK_Offline ⚙ → Play offline: downloads every file in the Service Worker's list into its cache, resumable
RWK_Report problem report: latest errors, audio state, files that failed to load
worker/ One Worker shared by every game
index.js static files + large files from R2 (with Range) + /api, each player's settings
auth.js verifies Cloudflare Access logins (JWT)
saves.js save API, with ETag overwrite protection, and save history
players.js counts the emails that save, for the deploy's reminder
toolkit.js the toolkit's part of js/plugins.js; serves a newer toolkit after upgrade
settings.js the deployer's settings from R2, so config applies without a redeploy
translate.js POST /api/translate: Workers AI (env.AI), each line kept in R2, a daily character capThe build
The game folder is never written to. Each run:
Picks the files: the standard RPG Maker folders, plus
include, minusexclude.Hard-links them into
<game drive>:\.rwk-build\(no extra disk space; copies on another drive).Adds the plugins to the build's copy of
js/plugins.jsand marksindex.htmlwith<meta name="generator" content="rpgmaker-web-toolkit">. Unless a public site asked to be listed: anoindexmeta tag,X-Robots-Tag: noindexand arobots.txt. Also writesrwk-sw.js, a Service Worker that keeps the pictures, sounds, data and scripts (each under 25 MiB) in the browser's Cache Storage, so a phone does not download them at every launch. It never touches/api/. A new deploy lists each file's version (size and modified time); on the next launch after all tabs of the game close, the worker drops only the files that changed. Players see what it holds, and can empty it, in ⚙ → Help → Game files on this device. Turn it off withcache.The worker also lists the files over 25 MiB (a second list,
LARGE, from R2). It never stores those on its own: only ⚙ → Play offline does.RWK_Offlineasks the worker for the list over a message channel, compares it with the keys in the cache, and downloads what is missing, three files at a time, with a request header that makes the worker leave them alone. It counts bytes through aTransformStream, so a big file moves the bar while it downloads, and stores each file only after a plain 200 from this site (never a login page Access redirected to).index.html,js/plugins.jsandrwk.webmanifestare answered from the network first and the cache when there is none (or after 4 seconds), so an installed copy opens offline. A cached file answers aRangerequest with a 206 (movies, streamed audio).rwk.webmanifestcarries the icon's real size,scopeanddisplay: fullscreen, which is what lets a browser offer to install the game.Uploads files over 25 MiB to R2, skipping unchanged ones by hash. The limits come from
cli/limits.json, downloaded frommainon each run.Deploys the Worker with the rest as static files (wrangler also uploads only changes).
Deletes the build folder.
Settings per player
The shared settings are written into js/plugins.js at build time. A config with players also gives the Worker a table { <hash of the email>: <settings that differ> } (player-settings.json next to worker.js), and routes js/plugins.js through the Worker. For each request of that file, the Worker verifies the Access login, looks up the hash, and merges that player's settings into the RWK_Core config it sends, with Cache-Control: private, no-cache. No login, or no entry: the file as built. GET /api/config returns the same entry, and the save API applies the player's saveHistory.
Settings without a redeploy
Each deploy, and each config after it, also writes the settings to R2 as rwk/settings/<game>.json: { "config": <everyone's RWK_Core settings>, "players": <the table above> }. The config part leaves out what the build itself is made of (toolkit version, game id, engine, cache, MV's media flags). The Worker always runs first for js/plugins.js: it reads that file (at most once every 30 seconds per Worker instance), puts config and then the player's own entry over the RWK_Core config it sends, and the save API takes saveHistory from it. The file wins over what the deploy built in; without one (R2 unreachable on the first read, or a game last deployed before this), the Worker uses the build's own settings. The creator's policy is applied before the file is written, so it still wins.
Update from the link
upgrade works without the game folder. Cloudflare cannot hand back a Worker's static files, and wrangler deploy always replaces all of them. So the CLI calls the Workers API itself: PATCH /accounts/{id}/workers/workers/{name}/versions/latest?deploy=true creates a new version from the latest one, and every field the patch leaves out (the static files among them) carries over.
The patch holds the new Worker, with the new RWK_*.js plugins inside it, and sets the asset routing so the Worker runs first for js/plugins.js and those plugins. The Worker serves the plugins itself and rewrites js/plugins.js: the game's own part stays, and the toolkit's part (after the rpgmaker-web-toolkit marker) is replaced. MV's phone audio / movie flags come from the deployed copy.
A script upload with keep_assets: true also keeps the files, but it keeps the old routing too, so the Worker never sees js/plugins.js.
WORKERS_API in cli/lib/workers-api.js records the endpoints and fields used, and the date they were checked. scripts/check-workers-api.js compares them with Cloudflare's published OpenAPI schema. The Workers API workflow runs it every Monday. When something the command relies on has changed, it opens one issue (label workers-api) that mentions the repository owner, and stays quiet until that issue is closed; it closes the issue itself once the check passes again. At run time, an API error, or a Deprecation / Sunset header from Cloudflare, prints that date.
Storage
Static files are Workers Static Assets (free, unmetered bandwidth). One R2 bucket, rpgmaker-web-toolkit, holds the rest:
rwk/games.json settings of every game (keyed by fingerprint)
assets/<slug>/<path> files over 25 MiB
assets/<slug>/.rwk-manifest.json hashes of those files
saves/<sha256(email)>/<slug>/<name> saves (config, global, file1…)
history/<sha256(email)>/<slug>/<name>/<time>
older copies of those saves (not config and global)
rwk/players/<slug>.json hashes of the emails that saved, with the first save time
rwk/translate/<model>/<from>-<to>/<sha256(text)>.txt
live translations, shared by every game (one per line of text)
rwk/translate/usage/<day>.json characters sent to Workers AI that day (UTC), for the daily cap<slug> is the site name the game was first deployed under (storageId after a rename), so a rename moves nothing.
Saves are stored under a hash of the email, never the email itself.
Save sync
At boot, before the engine reads a save, the plugin keeps the newer of each local / cloud pair and asks when both changed. Each save uploads about a second later with If-Match, so it never silently overwrites another device. What players see: Cloud saves.
Before the Worker overwrites or deletes a save, it copies the old one to history/: at most once per 15 minutes per save, unless another device or a delete replaces it. It then drops copies past saveHistory (keep per save, days old). Restoring copies one back over the save, after keeping the save it replaces.
Security
- Cloudflare Access blocks requests at Cloudflare's edge; the Worker also checks the login's signature to learn the email.
- Until Access is set up, every request gets the being set up page.
- Preview URLs are off (they bypass Access).
- The Cloudflare login has only the permissions it needs, in its own wrangler profile.
A game's rwk-policy.json is read on every run and can stop the deploy, keep the site private or turn features off.
Planned work: ROADMAP.md. Working on the toolkit: CONTRIBUTING.md.