Releases / v0.89.0

v0.89.0 Stay In Your Lane

2026-09-11

A script stays inside its project, a loop without an exit stops the script rather than the editor, a game reaches public addresses only, the editor no longer goes down with the terminal it was launched from, and a relay keeps its shape under load. Nothing here changes how you write a game; it draws lines a game could previously cross.

A path is relative to the project, and stays inside it

Every path a script or a scene file names — assets.getFile, assets.getContents, node.model =, a texture, an audio clip, a scene, a script name on a node, a terrain save folder — is read relative to the project folder and resolves inside it. One that would land anywhere else, whether spelled as an absolute path or climbed to through .., resolves to nothing, and the Console says which rule refused it, once per distinct reference.

A game runs on a machine that is not yours. What is on that machine's disk is not your game's business, and a scene file is data that may have been written by somebody else.

assets.getContents lists at most 20 000 files and says so when it stops, and it does not follow a linked folder. A linked package's folder still resolves, because that is where a linked package is.

A loop without an exit stops the script, not the editor

while true do end in update used to freeze the editor, the player and a dedicated server alike, forever, with nothing to say why.

A pass into your scripts may now run for 2 seconds — 500 ms on a dedicated server, adjustable with floptle serve --script-budget-ms. Past that, the script that overran is stopped with an error naming the budget, and it is not called again until you edit its file. Everything else keeps running.

The scripts share 512 MB between them. A table that only grows hits "not enough memory" in its own call, and the rest of the game goes on. A print inside a loop keeps a thousand lines a frame on the Console and reports the rest as a count, and a save slot holds at most 10 000 keys and 4 MB.

A game reaches public addresses only

http.* from a script connects to public addresses and nothing else. Loopback, private networks, mDNS names, .internal names and link-local addresses are refused, judged on what a name resolves to rather than on how it was spelled, and the refusal is res.error with the rule named.

The editor's Play is the one exception: hitting http://localhost:3000 while developing is the ordinary case and keeps working, and the Console says once per session that an exported game and a dedicated server will not allow it.

Redirects are no longer followed. A 3xx arrives as an ordinary reply with res.location, and the script decides. A request body is capped at 8 MB like a reply, and Host, Content-Length, Transfer-Encoding and Connection are the transport's headers to set.

A script acts as the player, not as the developer

account.* from a game reaches the player's surface — /wallet, /missions, /games/… and /me/… — and nothing else. The token behind those calls belongs to whoever is signed in to the Hub on that machine, and the developer endpoints under /cloud/… (keys, builds, usage) are not a game's to call on a player's behalf. A call to one is refused at the call site with the rule named.

A project's packages ask before they are trusted

A project that arrives with packages inside it — a folder or a zip whose packages/ carry manifests asking for Files or Network — loads them with no permissions the first time it opens, and a banner across the top of the editor names each package and what it asks for. Trust this project grants them and remembers that set of packages for that project; Keep them restricted leaves them loaded with nothing for the session. A changed manifest asks again.

A withheld API is still there: http.get in an untrusted package raises with the trust rule named, not as a package that forgot its manifest. floptle exec is not held to any of this — a script you named on your own command line is your own code.

The editor survives losing its terminal

Every line the engine writes to the terminal now survives the terminal going away — a closed window, a pipe nobody reads. The Hub launches the editor with its output in a log file of its own (logs/ under the Hub's data folder, the last five kept), which is also the file to send along with a bug report.

openUrl parses what it opens

openUrl, ed.openUrl and sys.openUrl parse the address before anything opens it: http or https, a host, no username or password, and nothing in it a shell could read as its own syntax. An address that fails is refused at the call with the reason, on every platform. On Windows the link is handed to the shell's own open call rather than through cmd.

A relay keeps its shape under load

Every relay, the one you self-host included, now refuses a few shapes: one lobby per connection, at most 4 096 lobbies and 64 clients in each, ten lobby opens and thirty joins a minute from one address, half a megabyte a second from one connection, and a lobby nobody has joined for half an hour ends unless its host is a dedicated server. A reliable message a client pushes at a host is capped at 64 KB, a host's at 128 KB — four times the largest prefab any shipped game spawns. A refusal is a sentence on the wire, and each is counted in the relay's report. floptle-relay --no-address-limits lifts the two per-address rates for a load test run from one machine.

A host checks what a client sends on receipt, not only at the sender's queue: an RPC value past the wire's guardrails is dropped, a server-only message from a client is ignored, and both are counted.

A managed relay is verified by name. A relay reached under fopull.com has to present a certificate for that name; a self-hosted relay or a direct host:port keeps the dev-trust model, where the lobby code is the secret. Until the managed relay's certificate is live, a connection that cannot verify it goes ahead on the older model and says so once in the Console. A later release will refuse instead.

A managed relay that cannot refresh its key list backs off — one second, doubling to its normal half-minute — and says so once when it starts, at most once a minute after, and once on recovery with the window in it. It kept serving through a nine-hour outage exactly as designed; it also retried fifteen times a second and wrote every attempt to a journal that then lost everything before the storm. That rate is gone.

docs/multiplayer.md now says the honest thing about a game key: a stranger with it can use up your plan's players, it cannot cost you money, and rotating it is what rotation is for.

For a server you run

A dedicated server's unit keeps its runtime and state directories to its own user, denies the process link-local addresses, and gives up a handful of capabilities a game server never uses. Its status file names the prefix of the game key rather than the key. A deployment row carrying a control character in any field is refused with the field named, rather than written into a unit file.

One VM

The vm-luajit escape hatch — buildable for one release after Luau became the default, as ADR-0028 scheduled — is removed. Luau is the only VM the engine embeds, native and browser alike. scripts/vm.sh still switches the code generator (luau-codegen) for benchmarking.

Upgrading

If a script of yours reads a file by absolute path or through .., it now reads nothing and the Console says so — move the file into the project and reference it relatively. If a script reaches localhost from an exported build, it is refused; in the editor's Play it still works. If http.get was relying on a redirect being followed, read res.location and make the second request yourself. If a game calls account.get("/cloud/…"), it is refused — that is a developer call, and the portal is where it belongs. If you open a project that carries packages asking for permissions, you will be asked once. If you run a relay and a load test from one machine, pass --no-address-limits to the relay. Nothing else needs a change.

Floptle Hub

Linux x86_64 7.5 MB Download → sha256 38b9023050084d766f649b20920e27e8b3b6e39d876640899a7dbcbc7fd20b04
macOS Apple Silicon 5.7 MB Download → sha256 2915379348859ff651c5ba4be3a6ae6e3bf5bd50dbb28da89295d271f7f924c1
macOS Intel 6.2 MB Download → sha256 9f73c84d81749293986eeab099e3f85c3291100694acbd484e8afab4d8f46e6d
Windows x86_64 7.1 MB Download → sha256 4eec9f65cf6296d15e0f2c6bbc4711b216537fc7f7c037480ccff22d019e6d47

Engine

Linux x86_64 33.7 MB Download → sha256 4df5b3e42a51f0a4198179c89211779c22a7e80553d7d7bcb7f701733c68d1e1
macOS Apple Silicon 27.4 MB Download → sha256 f2599fd70563d0b0c5b7eb288a73752d26c88d662aa77ba19d674bdb23db4b7a
macOS Intel 29.5 MB Download → sha256 928496d962a3a535453de55178f7144e181aabe839e24714ef84ae1c25f1c1d8
web 4.3 MB Download → sha256 c9db252b6579acf90663e81a0c41d720d32213aaad4116d3636076348bd70d51
Windows x86_64 33.3 MB Download → sha256 82a116523e2044980e1a8f5c3ddceb2f30fe829996fc9e14a639ce0f9dcd7114

The binaries are not signed yet. On macOS, right-click the app and pick Open the first time. On Windows, if SmartScreen appears, pick More info then Run anyway.