Squishy Butter
Press and hold a stick of butter and watch it sink. Real soft-body physics, no install required. 484 collectibles (butters, snacks, objects and mechanical clickers), each with its own feel, sound and certificate of authenticity, in English, Indonesian, Spanish, Portuguese and Vietnamese.
Solo-built and shipped on web, iOS, Android and Mac: 2,600+ downloads with zero ad spend.
- 2,600+ downloads, zero ad spend. 1,360 Android device acquisitions in the last 28 days, up 141%. 4.7 stars on the App Store (7 ratings).
- More than 1,400 automated tests on pulls, payments, permissions and the game itself: 802 Vitest, 219 Deno function tests and 400+ database checks. They caught four real money bugs, each fixed and deployed the same day.
- The catalogue grew from 214 to 484 collectibles since late October without new engine code per item: snacks, objects and mechanical clickers are data plus a procedurally built Blender model.
- Five languages for the 69% of players who don't use English. Shipped solo to web, iPhone, iPad, Android, Mac and Apple Watch, plus VR in the headset browser.
- Honest gap: only 3.6% of signed-in players come back the next day. Retention is the current focus, with cohort tracking in place to measure each change.
The Idea
It started with a gift. Someone handed me a squishy butter toy and I immediately understood why people were obsessed with them.
Physical squishies were selling out at fairs and people were queueing for restocks. If people were this excited about a foam rectangle that does nothing, a digital version with real physics, a collection mechanic and something to show for it could work.
But almost every squishy thing on the web is a scale animation: the object shrinks toward its middle and springs back, and it feels like rubber. I wanted the feeling of a fingertip actually sinking into something soft. That's a materials problem, not an animation problem, so it needed a real deformation model running on the graphics card. The squish counter was deliberate too. Knowing you've squeezed something 12,000 times makes it yours.
See It Play
The App Store preview in 30 seconds: squish, switch skins, open a Dairy Delivery, reveal a numbered Legendary, then find it in the collection.
One Squish Engine, 484 Collectibles
Four decisions do most of the work, and you notice each one in the first three seconds without being able to name it.
- Jab it and it resists. Lean on it and it gives. Resistance rises with the square of how fast you're pressing, which is what separates memory foam from rubber.
- Hold it and it keeps giving. The target depth drifts lower the longer your finger stays down, so the butter never hits a wall and stops.
- Let go and the dent stays. It sits for a couple of seconds, then eases back without bouncing. A spring would overshoot, and overshoot is exactly what reads as jelly.
- Press in several places at once. Every dent is its own little simulation, and each fades on its own schedule.
Deformation runs in a GLSL vertex shader. The CPU evolves each dent's depth and velocity every frame and uploads them; the GPU only does the geometry math. The number of dents is compiled into the shader, and asking for too many makes it silently fail on lower-end hardware, which renders as a blank canvas. So before compiling, the engine asks the device what it can afford:
The phone cap is 20 because 48 was measurably slower on iOS.
148 of the skins are also bundled iMessage stickers. Hover to pause the lineup.
A collectible is data plus a look: a compressed texture map or a generator that paints one at runtime, plus its own material, physics and sound. The same engine, shaders, sound model and certificate system drive all of them, so adding one means describing it, not building a new feature. That's what let the catalogue grow from 214 stick skins in October to 484 items today: 210 butters, 166 snacks, 39 objects and 69 clickers.
Some skins push the engine further. Frozen barely yields until you warm it, and it warms where you touch it, so one end can soften while the rest stays solid. Fur is 16 stacked shell layers, each run through the squish shader so the coat dents with the butter. Gold reflects the actual room instead of painted highlights. Sound is generated live in Web Audio and fed the dent's depth and speed every frame, so it tracks what your hand is doing.
Clickers, Objects and the Model Pipeline
Players asked for more than butter, so the engine learned shapes. A soft snack still runs through the squish shader. A clicker is the opposite problem: it has to feel mechanical and must not squish at all. So each model carries named parts, and the catalogue entry says what each part does:
- Press. A key, a big arcade button or each of a calculator's 16 keys springs down and back on its own. The spring is sub-stepped, because at 30 fps a single step let a keycap fly off.
- Pop and toggle. Push-pop bubbles invert one at a time and reset when all are done. A light switch, a click pen and a flip phone swap between two models without the object hopping.
- Drag. A joystick tilts and springs back, a combination-lock dial spins, a kitchen timer winds up and unwinds to a ding. On the computer mouse, a drag only grabs if it lands on the scroll wheel; it rolls with ticks and coasts after a flick.
- Rigid parts. Any mesh named
rigid_*doesn't deform, so the mashed potatoes squish while the bowl stays a bowl.
The models are built by headless Blender scripts rather than sculpted by hand, so a new shape or colourway is a code change that can be reviewed and rebuilt. When the 61 models reached 93 MB and pushed the Android bundle to 116 MB, I added Draco compression to the pipeline: 93 MB down to 8.1 MB, with part names and transforms identical so every press and pop still worked. The decoder runs as plain JavaScript because the site's content security policy doesn't allow WebAssembly.
The Pull System and Its Economy
Blind bags and mystery boxes were already dominating toy retail (Stanley cups, Sonny Angels, Pop Mart figures), and the same mechanic was proven in mobile games. Nobody had applied it to a browser-based tactile toy, and a butter you discover through a pull feels different from one you pick off a shelf.
You buy coins, then spend them on pulls that draw from the full rarity pool with published odds. Pulls skip butters you already own. An Epic or better is guaranteed within 30 pulls and a Legendary within 90. New accounts start with 300 coins, enough for three pulls, and each pull ends in a card reveal keyed to the rarity you got. Every skin carries a serial number, a provenance story, a certificate and a trading card.
Base rates across the 382-item pull pool as of November 2026. The game loads them live from the database, and each drop of new items is weighted so these shares stay the same. A script compares the app catalogue with the live pool before every release.
Pulls run entirely on the server. One Postgres transaction locks the player's row, deducts the coins, applies the pity counters, draws only from skins the player doesn't own, assigns the next serial and refuses the pull if a limited skin has sold out. All or nothing. The client doesn't decide what you get or how many coins you have.
Before a release I audited the economy end to end and found real holes. A server-only function that credits purchases had never had its default public access revoked, so in principle anyone with the public API key could grant themselves coins. And because players could edit their own profile row, they could reset the last daily claim and collect the same reward again. One migration locked every coin-granting function to the server and turned the profile guard from a block-list into an allow-list. Purchases are now capped by what the store actually reports, so a fabricated transaction ID can't mint coins.
The same pattern now covers more ways to pay. Every purchase, whether a Stripe checkout, a StoreKit transaction or a RevenueCat receipt, is verified on the server and credited inside one database transaction keyed by the store's transaction ID, so a retried webhook or a replayed receipt credits nothing twice. Limited holiday bundles grant their butters and coins in that same transaction and skip butters you already own. Refund take-backs are written and tested for the next payments release. Coin packs cost about half the US price in Indonesia, Brazil and the Philippines on both stores, where a large share of players live.
To test all of this without touching real players, I rebuilt the backend so a complete local copy comes up with one command, seeded with the full catalogue and test accounts. Now 22 SQL test files with more than 400 checks cover pulls, payments, bundles, permissions, consent and account deletion, 219 Deno tests run the server functions against forged and replayed store receipts, and 802 Vitest tests cover the game code. One command runs every suite locally before each push. The tests found four more real bugs, all fixed and deployed the same day, each with the test that proves it: limited skins could sell past their supply cap, two players pulling at the same instant could get the same serial number, buying the same coin pack on Mac and then iPhone didn't credit the second one, and a web bank transfer was credited before the money arrived.
Bringing Players Back
Downloads grew, but most players squished for one day and left. I added a cohort query (signups by day, then whether each player came back on day 1 and day 7, split by platform) and measured a baseline before changing anything: 3.6% day-1 return and 0.9% day-7 for signed-in players who joined between October 6 and November 2. Those numbers are low, and they're useful: every feature below is now judged against them instead of against downloads.
- Squisher Levels. An account level from 1 to 50 with coin rewards and a free case every five levels. XP is derived on the server from what players already do (squishes capped per day, days played, collection, spins, finished sets), so there is no stored XP to drift or tamper with. Levels show as badges on the leaderboard.
- Daily spin. One free spin a day with the odds printed under the wheel and a streak bonus. Spins can't be bought.
- Friends. Add someone only by a link or six-character code they chose to share: no chat and no username search, because the audience skews young. Block works at any time and looks like a wrong code to the blocked player. Ships in the next release.
- Rewarded videos. An optional video for coins in the iOS and Android apps. Coins are credited only after an edge function verifies Google's signed server-side callback, the daily cap is enforced in the database, ads are non-personalised, and iOS shows no tracking prompt. Too early for revenue numbers.
- Celebrations sized to the moment. One shared component for pulls, level-ups, purchases and unlocks with three tiers of particles, sound and haptics, so a Legendary feels bigger than a daily spin. It respects reduced motion.
- A better notification ask. Instead of the system prompt on launch, the apps ask right after a good moment such as a first unlock or a level-up, and back off if the player says not now.
Current App Store screenshots, rendered from the running app.
What Was Hard
Getting the feel right was mostly deleting things that were subtly wrong: a spring on release, a dent that never deepened, a drag that slid one divot around instead of leaving a trail. Each was defensible on its own, and each made the butter feel like a different material.
The worst bug looked correct in the code. The spacing check measured the gap between the cursor and the active dent, but that dent eases toward the cursor every frame, so the distance never grew large enough to trigger. Below a certain speed, dragging left no trail at all. Simulating the loop outside the browser found it in one pass.
Where It Stands
Android launched September 16 with no paid promotion, and Google Play's recommendation rows did most of the work. Almost all of that traffic came through browse surfaces rather than search: people install an interesting icon and only about a third open it. Acquisition is no longer the bottleneck; keeping players past day one is. A first-screenshot A/B test is set up on the Play listing, and the retention features above ship to the apps in the next release so their effect shows up against the baseline.
What I'd do differently. Set up a local database from day one; for months every change was tested against the live project. Treat the economy as adversarial from the first coin, because every hole I closed came from trusting the client somewhere. Check store state by API before every release: an audit of App Store Connect found the iOS coin packs had never been submitted for review, so they go in with the next iOS version. And measure retention before growth; I tracked downloads for weeks before building the cohort query that showed where players drop off.
Also Built
- Auth. Sign in with Apple, Google or an eight-digit email code, chosen over a magic link so you stay in the same app whichever email client opens. Profiles track coins, squishes and time squished, with 38 colours, 59 expressions and unlockable titles.
- Five languages. About 69% of players don't use English, so in October I translated everything into Indonesian, Spanish and Portuguese, including about 5,000 lines of skin lore per language, then added Vietnamese. A screen smoke test checks every screen in each language at phone and desktop size.
- Beyond the phone. Three home-screen widget sizes plus Lock Screen widgets, an Apple Watch app that keeps your count on your wrist, 148 butters as iMessage stickers, and an App Clip that lets someone squish before installing.
- Reminders and privacy. Push reminders arrive in the player's own language. Email opt-in keeps a consent record, EU and UK visitors get a cookie choice, and the app ships an Apple privacy manifest.
- VR on Vision Pro, Quest and Android XR. Open squishybutter.app in the headset browser and pinch to squish across five themed rooms. Small items are fitted to a reachable size on entry and both hands resize them. A VR room with your collection shelf, cases, the daily spin and the leaderboard is built for the next release. It's WebXR, so there's no native port, and an automated test drives the flow in an emulated Quest headset.
- On every screen. One Capacitor and Three.js codebase for web, iPhone, iPad, Android, Mac and Apple Watch, with store screenshots in five languages rendered from the running app by a headless-browser pipeline.
- The landing page. A full product page ↗ with the collection, rarity tiers, drop rates and coin packs, responsive down to a phone.
- Lighthouse. The landing page scores 99 Performance and 100 for Accessibility, Best Practices and SEO. The app scores 100 on the first three and 92 on SEO, because crawlers can't read content drawn on a GPU canvas.
Full details: sign-in, languages, VR, every screen and the landing page
Auth
Sign in with Apple, Sign in with Google, or an eight-digit email code - no password to forget or rotate. The magic link pattern was ruled out early: a tapped link that opens a new browser tab breaks the flow, and the right email client on iOS isn't guaranteed to match the browser. A one-time code sidesteps both problems - the user stays in the same app, on the same page, and nothing depends on which client they open.
Once you're in, your profile tracks coins, squish count, and total time squished. You can set a username, pick your butter's color from 38 options, choose from 59 expressions, and claim a title - starting with Fresh Out the Fridge and unlocking through Squish Enthusiast, Butter Baron, and beyond as your squish count, time, and collection grow.
Five Languages
About 69% of players use Indonesian, Spanish or Portuguese, so an English-only app was talking past most of its audience. In October I translated all of it into those three languages. The interface shipped on October 22, and the written lore for all 214 skins at the time, the provenance stories on every certificate and trading card, went live on October 23. Vietnamese followed a week later. Widgets, iOS permission prompts, push reminders and the store listings are translated too.
The lore was the big piece: about 5,000 lines per language. Longer Spanish and Portuguese strings also broke layouts that fit in English, so the certificate stamp and footer had to be refitted for phones. To keep that from coming back, a screen smoke test checks every screen in each language at two viewport sizes, alongside the Vitest unit suite.
The store art followed. The screenshot pipeline takes a language flag, loads the app in that language and sets the headlines in it, so each translated listing gets the app the way those players see it: 120 localized screenshots across iPhone, iPad, Android and Mac.
Beyond the Phone
The same account follows you off the main screen: home-screen and Lock Screen widgets show your squish stats, the Apple Watch app keeps your count on your wrist, and 148 butters ship as iMessage stickers bundled with the app.
Squishy Butter in VR: Vision Pro, Quest and Android XR
Open squishybutter.app in the headset's browser and tap Enter Immersive. The butter sits at real size, about 12 cm, on a counter a metre off the floor, close enough to reach. Pinch to squish: the pinch is cast as a ray onto the butter and pressed exactly like a fingertip, so the same GPU dents, sounds and skins carry over. You can also pick the butter up, carry it and set it back down, and a small sign on the counter switches between five themed rooms without leaving immersive mode.
It's WebXR in the browser, so there's no native port and no separate app to install. A native Vision Pro wrapper was prototyped and dropped, because WebXR doesn't run inside a WKWebView. An automated test drives the whole flow in an emulated Quest headset: enter, squish, lift, move, and cycle every room.





Headset view of each room, captured from the automated test running an emulated Quest 3.
The next step turns the room into the game. A shelf shows your collection 12 at a time and equips with a pinch, a Dairy Delivery box on the counter opens a real server pull, the daily spin wheel and the leaderboard (everyone or friends) stand beside the counter, and moving parts work by hand: pinch a joystick to tilt it, turn a lock dial or kitchen timer by the angle of your hand, flick the mouse wheel. All of it is a separate 38 kB chunk that loads only when you enter VR, so the flat game pays nothing for it. The emulated-headset test grew to 58 checks.
On Every Screen
One Capacitor and Three.js codebase runs on the web, iPhone, iPad, Android, Mac and Apple Watch, and through WebXR on Vision Pro, Quest and Android XR, with the same account, collection and coins everywhere. These are the current store screenshots, rendered from the running app by a headless-browser pipeline, so every size stays in sync with the real UI.
The butter gets more room to breathe, and the wider canvas makes squishing feel even more tactile.
Both displays get their own set: the outer screen shows the phone layout, and the inner screen gets the roomier tablet layout.
A lightweight native window around the web build, with StoreKit purchases, so your collection, coins and squish count match every other device.
The Landing Page
The full product page - collection, mechanics, rarity tiers, and the order flow.
Mobile Experience
The landing page is fully responsive - the collection, rarity tiers, and order flow all work on a phone with no install required.
Recorded from the live site
Lighthouse & Link Preview
Both pages were audited with Google Lighthouse. The landing page scores 99 Performance and 100 for Accessibility, Best Practices and SEO. The app scores 100 on the first three and 92 on SEO, because crawlers can't read content drawn on a GPU canvas. Shared links unfurl with a custom preview card:
Need Something Like This?
I took this from an idea to a live product on its own: the 3D engine, the database and payments, the iOS, Android and Mac releases, the store listings and the analytics. If your team or product needs that range, from shader work to server-verified purchases to getting through App Review, let's talk.