Overview
Highsky turns advertising space into a shared, interactive sky. Companies claim branded balloons, and another advertiser can outbid them to pop the existing balloon and replace it with their own.
I developed the interaction between the animated scene and the purchasing flow, then refined how it handled payment confirmation and ownership changes. The main challenges were coordinating balloon transitions, rendering the changing sky, and keeping visitors' views consistent with confirmed purchases.
The challenge
The central interaction had to do more than look convincing. A balloon represents an owned advertising position, so its animation needs to reflect a real change of owner. Opening checkout or returning from Stripe cannot, on its own, mean that the purchase succeeded.
The environment mattered too. Balloons needed stable positions within a sky visitors could pan and zoom, while floating motion and changing colors made the scene feel alive. The challenge was connecting those visual behaviors to application state without turning every animation frame into a React update.
Product experience
Visitors explore the sky, inspect a balloon, and follow its advertiser's link. Someone buying a spot enters their campaign details and completes checkout. Claimed spots require a bid of at least 1.5 times the current price.
After a confirmed replacement, the old balloon compresses, shakes, and bursts into particles. The new owner's balloon inflates in the same position, preserving the identity of the spot. A full-sky takeover extends that mechanic across the inventory.
Architecture
The project started from Plainform, with Next.js handling the application and payment routes. React manages forms, drawers, selection, and controls. PixiJS renders the animated sky, clouds, and balloons, with scene updates running through its ticker.
PostgreSQL and Prisma store spots, campaigns, bids, and ownership history. Payment handlers recheck the current bid under a database lock before capture and transfer. Successful ownership transactions also write an inventory event, which connects the persisted change to visitors' views.
Built with
- React
- TypeScript
- Next.js
- PixiJS
- PostgreSQL
- Prisma
- Stripe
- Tailwind CSS
- Shadcn UI
- GitHub
- Supabase
- PostHog
Engineering decisions
Replace the owner at the right moment in the animation
Changing a balloon's data immediately would make the new brand appear before the old one had finished popping. The scene instead holds a pending configuration while playing the outgoing animation, then applies it when inflation begins.
The spot keeps its position throughout. Scaling, fading, movement, and particle fragments create the transition around that stable identity. This lets the same scene operation handle both an individual outbid and the balloons changed by a full takeover.
RECORDING — a confirmed balloon replacement
Capture the existing balloon popping and the replacement inflating in the same spot. Keep enough of the sky visible to show the changing background too. A real screen recording will explain these moving interactions better than a still screenshot.
Caption: “A confirmed purchase replaces the balloon's owner while preserving its position in the sky.”
Blend the sky with a shader
The changing background uses a custom fragment shader rendered through PixiJS. It blends cool blues with warmer peach and coral tones, moving the transition diagonally across the screen rather than switching the whole background at once.
The shader calculates the blend relative to the viewport's aspect ratio, eases its progression, and adds subtle dithering to reduce visible color banding. The automatic cycle runs over 36 seconds. Keeping that work in the renderer lets the background change alongside the scene without driving React renders for every frame.
Recheck competing bids before capturing payment
Two buyers can enter checkout against the same displayed price. The payment handler takes a database lock for the spot and rechecks the minimum bid before capture, so the original checkout amount is not assumed to remain valid. If a completed purchase has raised the minimum above another buyer's bid, that bid is marked failed, its campaign is rejected, and the handler cancels the payment authorization instead of transferring ownership.
Notify visitors, then fetch the confirmed inventory
The current implementation uses Supabase Realtime to notify open visitors when an ownership transaction creates a SkyInventoryEvent. Clients fetch fresh inventory, compare it with their current balloons, and play transitions for the owners that changed.
The notification is a signal to read the database-backed state. Cache invalidation also refreshes the server's map and leaderboard data. A full takeover emits one event for the transaction, rather than one notification for every balloon, and nearby refresh requests are debounced.
This flow runs independently of the buyer returning to the homepage. Other visitors can receive the update as soon as the ownership transaction completes.
Problems & iterations
Returning from checkout before the update is ready
The buyer can arrive back from Stripe while the payment webhook is still processing. The return flow preserves a snapshot of the previous balloon and polls the purchase-status endpoint, retrying up to 30 times with a one-second delay between attempts.
Once the server reports a paid purchase, the client retrieves the confirmed inventory and plays the replacement. Live synchronization is temporarily paused for that browser so it does not overwrite the old balloon before the transition starts. Other visitors continue receiving their own updates.
This is separate from ordinary visitor synchronization: polling is used to confirm the returning buyer's purchase, while Realtime events trigger inventory refreshes elsewhere.
Results & next steps
The result is a working MVP connecting an explorable PixiJS scene, competitive spot purchases, and visible ownership transitions. Subsequent iterations addressed the timing of payment confirmation and shared inventory updates, including cases where the buyer returns before the payment webhook finishes processing.
Highsky remains an experiment rather than an ongoing product roadmap. Its value for me was exploring a different kind of interface and working through the point where an animation has to agree with a payment and a database-backed change.
