Digging up the 2022 BalloonBlox: PHP, jQuery, and one database table per user
Before rebuilding, we excavated the original Party City-era prototype: a PHP/MySQL cart synced to a Roblox place over polling HTTP. It was charming. It was also terrifying.
BalloonBlox started as a pitch to Party City: their web 'balloon bouquet builder' let you compose a bouquet in 2D — we thought it would be better in 3D, in Roblox, where the bouquet appears tied to your character's hand as you build it on the web. Party City filed for bankruptcy; the prototype survived on a Linode box that has since gone dark.
The original repo is a time capsule: PHP endpoints, a jQuery UI drag-and-drop cart, and a Roblox place file with a single server script doing all the work. Step one of the rebuild was extracting the Lua from the binary .rbxl place files (via Lune's roblox library) and reading everything.
The good ideas worth keeping
Change detection by hash: the game polled get_cart_hash.php (an md5 of the cart contents) every few seconds and only re-fetched the full cart when the hash moved. Cheap and effective — we keep the idea, but as a monotonic integer version column instead of md5-of-serialized-PHP-array.
Physical shopping: stand in the red zone for 10 seconds and a red balloon is added to your cart — in the database, and in your hand, tied with a visible RopeConstraint and floated with an anti-gravity force. Walk into the yellow zone to let everything go. That's the soul of the thing and it survives intact.
The parts that did not survive review
function createSCTable($conn, $robloxUserName) {
$query = "CREATE TABLE $robloxUserName (id int not null auto_increment,
item_name varchar(255), primary key (id))";
...
}
function addToShoppingCart($conn, $robloxUserName, $item_name) {
$sql = "insert into $robloxUserName (item_name) values ('$item_name')";
...
}Every user got their own MySQL table, created on first sight of a username typed into a form with zero auth — and every query string-interpolates that username. SQL injection as a feature. The new backend is one carts row per player, parameterized queries via Drizzle, and a link-code flow so only someone standing in the game can claim a player's cart.
local playerName = ""
game.Players.PlayerAdded:Connect(function(thisPlayer)
playerName = thisPlayer.Name -- last player to join wins!
_G.pl = getPlayerFromName(playerName).Character
end)One global playerName for the whole server: whoever joined last owned everyone's balloons. The rebuild is server-authoritative and per-player from the ground up — every cart, zone timer, and balloon bundle is keyed by UserId.
Also not making the cut: two drivable cars (A-Chassis), a mansion, and in-car radios preloaded with several hundred copyrighted songs. The new place is a focused balloon shop with original, generated music.























