Event Management Platform, Dubai
Synerggy Events
A full rebuild of a Dubai event management company's web presence as a four-app platform: a cinematic public site where every section is editable with live preview, an admin panel, a podcast studio back office, and a separate live lucky-draw app that runs on a big screen at the event itself.

- Role
- Sole developer: design direction, four apps, API, database, 3D, and infrastructure
- Timeline
- 2025 to present
- Status
- Live in production
Synerggy Events produce corporate summits, cultural festivals, and live concerts across the UAE and the GCC. They were on a static legacy site that had to go through a developer for every content change, which for a company whose entire calendar turns over every few weeks is close to unusable.
The rebuild inverted that. Every section of every page is editable from an admin panel with a live preview of the real site, and the pieces that used to be one-off developer builds (a campaign landing page, a contest, an event takeover of the homepage) became things the team configures themselves.
- Deployable apps
- 4Deployable apps
- Database models
- 39Database models
- API route groups
- 24API route groups
- 3D mic, down from 2.31MB
- 435KB3D mic, down from 2.31MB
The system
What it is made of
| App | Role | Stack |
|---|---|---|
| web | Public marketing site | Next.js 15, Framer Motion, Lenis |
| api | REST API and CMS backend | Fastify 5, Prisma, MySQL |
| admin | Content, events, contests, marketing, studio | Next.js 15, JWT |
| draw | Live lucky draw on the event screen | Next.js 15, scoped JWT |
Content
Everything editable, with a live preview of the real thing
Every section's copy, imagery, and calls to action are database rows rather than hardcoded markup: the hero, the about block, services, team, the closing call to action, the footer, contact details, and every dynamic page behind a slug.
The preview is the part that took the design work. Rather than reimplementing each section inside the admin panel (which guarantees the preview eventually drifts from production), the admin frames a dedicated preview route on the actual website in an iframe. What the editor sees is the real component tree rendering the real content, with a desktop and mobile viewport toggle and a full-screen mode.
That preview surface is treated as internal: it is marked noindex, it hides the site's own header and footer so only the edited section shows, and it switches off analytics inside the frame so editors clicking around never pollute the site's traffic data.
The same pane powers the email builder, where it renders the exact HTML the API will send rather than a React approximation of it, so what the marketer approves is what lands in the inbox.
The hardest interaction
An opening sequence that reorders itself around a live event
The original plan was a three-part opening: hero, then a scroll-scrubbed video, then the first content section. That works right up until the company has an event to sell, at which point the most important thing on the page is the event and the brand hero is in its way.
So it was reworked to reorder itself. When any event is flagged to show in the hero, the opening becomes event showcase, then video, then the hero revealing behind it. With nothing flagged, or if the API is unreachable, it falls back to the original two-act sequence. One checkbox per event drives the whole thing, and the team can flip it themselves.
Both sequences are one sticky viewport with a long scroll track behind it, and scroll progress drives opacity, scale, and the video's playback position through overlapping ranges. The track length differs between the two versions because the three-act one has an extra reveal to give room to.
Scroll-scrubbed, not autoplayed
The video's current time is driven by scroll position inside a requestAnimationFrame loop that eases toward the target rather than snapping to it, so scrubbing reads as smooth motion instead of a jumping filmstrip.
Genuinely separate encodes
Landscape and portrait are two different files, swapped at the breakpoint, not one wide video cropped by CSS. The scrub loop tracks whichever one is actually on screen.
Warmed before it is needed
Each video seeks to a fraction of a second on metadata load, so the first frame is decoded and ready before the viewer scrolls far enough to see it.
Built for small viewports
The sticky stage is sized in small and dynamic viewport units with a support query, so mobile browser chrome appearing and disappearing cannot break the composition. It was tuned down to the smallest viewports still shipping.
Compressed hard, on a CDN
Both encodes were compressed well past the usual point without visible quality loss and are served from the CDN, with the local files kept as an automatic fallback if a CDN fetch fails.
Reduced motion is a real path
Anyone who has asked their system for less motion gets the showcase and hero as plain stacked sections with no sticky stage and no video at all.
The centrepiece
A 3D podcast hero that had to be premium and tiny
The podcast studio page needed to feel like the studio itself rather than a page about it. A microphone arrives from the right, and as you scroll the camera moves through the space to settle on an empty chair with the mic angled toward it: the seat is waiting for a guest, and nobody has to be told that in a sentence.
The constraint was brutal in both directions. The assets had to download on a slow connection and render on a cheap phone, but a podcast studio selling premium recording time cannot ship a hero that looks low-poly. The microphone started at 2.31 MB and shipped at 435 KB, with meshopt-compressed geometry and 1K WebP textures. The chair is 329 KB. Neither costs the client anything extra to decode: the standard loader already ships the Meshopt decoder, and the WebP texture extension is handled natively.
Getting it to run everywhere took more care than getting it to look right. The page probes for WebGL2 before mounting anything, then explicitly releases that probe context, because mobile GPUs cap how many live contexts a page may hold, sometimes at eight, and a leaked probe will starve the real canvas into throwing. If the probe fails, or the visitor prefers reduced motion, a static hero renders instead. An error boundary catches anything that still goes wrong at runtime and falls back to the same static version.
The render loop stops
An intersection observer pauses rendering entirely once the hero scrolls away, because the page continues for several sections below it and there is no reason to keep a GPU busy off-screen.
No hydration mismatch
The reduced-motion and WebGL decisions resolve in effects rather than during render, so the server HTML and the first client render always agree on which hero they are drawing.
Preload that actually preloads
The model URLs are preloaded from the page, and the preload only helps if the strings match the loader's requests byte for byte, including the cache-busting query, so both are generated from one source.
Observer follows the node
The observer attaches through a callback ref rather than a plain one, so if React ever swaps the element the observer moves with it instead of pinning visibility to false forever.
A CDN detail worth knowing: the zone in front of these files ignores query strings when building its cache key, so a version query busts the browser cache but never the edge. Replacing a 3D asset means a new filename, and a filename requested before it was uploaded stays poisoned by the cached 404 for a day.
Podcast studio
A studio back office, not just a booking form
The public page sells sessions, picks a backdrop wall, and captures a detailed booking with the package the guest wants. Behind it sits enough tooling to actually run a recording studio's calendar.
Requests and sessions
Booking requests arrive with an event trail against them, get converted into scheduled sessions, and appear on a calendar view of the studio's availability.
Customers and passes
Per-customer records with history, plus credit packages and a credit ledger so a client can buy a block of sessions and draw them down over time.
Invoices
Invoices generated against sessions and packages, tracked from the same panel rather than a separate accounting tool.
Email templates and logs
Templated studio correspondence with a record of what was actually sent to whom, previewed as real HTML before it goes out.
Packages and walls
The bookable packages and the backdrop walls shown on the public page are records the studio edits, so pricing and set changes never need a developer.
Studio settings
Operating hours, availability rules, and the configuration the booking flow validates against, all editable in place.
Contests
A lucky draw that cannot fail on stage
Contests are fully dynamic: custom form fields, prize tiers, page sections, partner branding, registration windows, one-entry-per-email enforcement, per-contest booking reference prefixes, and their own meta title and description. Entries collect into the same lead system as everything else.
The draw itself is a separate application, because the failure mode is a room full of people watching a screen. It runs as two windows on one machine (an operator console driving a fullscreen presentation window), and they communicate over a same-machine channel rather than the network. A venue wifi drop cannot freeze a reveal halfway through.
Fairness had to be demonstrable rather than asserted. Selection happens server-side using a seeded HMAC-SHA256 stream and an unbiased Fisher-Yates shuffle. The seed's hash is displayed before the first round and the seed itself released after the last, so anybody can recompute the result afterwards and check it.
Round one keeps 10%
The full entry pool is cut to a tenth. Percentages are clamped so a round can never reduce the pool below five people, however small the contest.
Round two keeps 20% of those
A five thousand entry contest runs five thousand, to five hundred, to one hundred, and into the final round.
The final round ranks five
One winner and four ranked backups. Ranks are never rewritten, so the record of what was actually drawn survives whatever happens next.
A failed claim promotes the next rank
If a winner cannot be reached or their claim does not check out, the operator marks it invalid with a reason and the prize falls to the next rank. That panel is operator-side only and never reaches the projector.
Rehearsals on real data
Unlimited test runs against the actual entries, watermarked across the entire presentation screen so a practice can never be mistaken for the real thing. Only one live draw per contest may complete.
Operators are not admins
A contest organiser gets their own account whose token is scoped to the draw and rejected by every admin route. They can run their own draw without any access to the business.
The draw app never sees the data
Its API returns an entrant's name and booking reference and nothing else. Email, phone, emirate, company, and custom answers are never sent to the machine on stage at all.
State survives a crash
The big-screen title override lives on the draw record rather than in the browser, so a laptop that dies at eight o'clock comes back up still configured the way it was rehearsed.
Leads & marketing
Every enquiry lands in one place, and can be campaigned to
Lead capture can be switched on per event, and captured leads list in the admin panel. Contest registrations, booking enquiries, and spreadsheet imports all resolve into a single lead record that keeps a trace of every origin it arrived through, so the same person entering two contests and booking a session is one lead with three origins rather than three duplicates.
The campaign engine is built for deliverability rather than blast volume. A campaign has a daily cap, a minimum gap between individual sends, a send window expressed in minutes past midnight, permitted days of the week, an out-of-hours override, and a priority for when several campaigns are competing. Sends are tracked per recipient with an unsubscribe token, and a suppression list is honoured across every campaign.
Reusable templates
Subject, preheader, heading, body, call to action, and footer as named templates that campaigns inherit and can override.
Audience targeting
Campaign audiences are stored as structured criteria against the lead set rather than a frozen list, and UTM tagging attributes what comes back.
Import with a receipt
Spreadsheet imports record how many rows were created, merged, skipped, and invalid, with the errors retained, so a bad import can be understood rather than guessed at.
Planned next
WhatsApp and SMS campaigns over the same lead set, audience rules, and suppression logic that email already uses.
Infrastructure
Four containers on one VPS, hidden behind Cloudflare
All four apps deploy from the same repository as separate services, each building from the repository root so the shared type package resolves correctly. Multi-stage builds with standalone Next.js output brought the images from roughly two gigabytes down to between 150 and 250 megabytes each. A push to the repository rebuilds and rolls the affected containers with no manual SSH step.
Nothing is exposed directly. Traffic reaches the VPS through Cloudflare Zero Trust tunnels, so the origin IP is never public and DDoS mitigation and TLS terminate at the edge. Media sits in object storage behind the CDN, page HTML is edge-cached, and the site revalidates its API fetches on a short interval so content stays current without hammering the origin.
- Public site
- synerggyevents.com
- API
- api.synerggyevents.com
- Admin
- admin.synerggyevents.com
- Draw
- stage.synerggyevents.com
- Media CDN
- cdn.synerggyevents.com
The old site was a Laravel application. A one-time migrator moved categories, portfolio items, services, site content, events, and admin users across, normalising media paths on the way, so nothing had to be re-entered by hand.
Hard parts
What actually took the time
Reordering the opening without rebuilding it twice
- Problem
- The brand hero and a live event both want to be the first thing a visitor sees, and which one should win changes week to week depending on what is on sale.
- Approach
- One sticky stage that composes either sequence from the same pieces, choosing between a two-act and a three-act arrangement based on a single per-event flag, with independent scroll ranges for each arrangement.
- Outcome
- The team reorders their own homepage with a checkbox. An empty flag set, or an unreachable API, degrades to the evergreen opening rather than to a blank screen.
3D that looks expensive and downloads like it is not
- Problem
- A premium studio needs a hero that reads as premium, but 3D assets that look good are usually megabytes and assume a desktop GPU.
- Approach
- Meshopt-compressed geometry with 1K WebP textures, served from the CDN and preloaded with byte-identical URLs, behind a WebGL2 capability probe that releases its own context, with a static hero and an error boundary as fallbacks.
- Outcome
- The microphone went from 2.31 MB to 435 KB with no visible quality loss, the render loop stops when off-screen, and devices that cannot run it get a deliberate static version instead of a broken canvas.
A prize draw with an audience watching
- Problem
- A live draw has to be verifiably fair, survive venue wifi, be rehearsable without contaminating the real result, and be operable by the contest organiser rather than by me.
- Approach
- Server-side seeded selection with the seed hash published in advance, two windows communicating on-machine so the network is not in the path, watermarked rehearsals on real data, and separate operator accounts whose tokens no admin route will accept.
- Outcome
- Any result can be recomputed and checked afterwards, a connection drop cannot stall a reveal, and the machine on stage never receives an entrant's contact details in the first place.
Stack
Everything it runs on
Public site
- Next.js 15
- React 19
- Tailwind CSS
- Framer Motion
- Lenis
3D
- Three.js
- React Three Fiber
- drei
- Meshopt
- glTF / WebP textures
Backend
- Fastify 5
- Prisma
- MySQL
- Zod
- JWT
Admin & draw
- Next.js 15
- Live preview iframes
- BroadcastChannel
- Scoped tokens
Messaging
- Resend
- SMTP fallback
- Templated email
- Unsubscribe tokens
Infrastructure
- Turborepo
- Docker
- Dokploy
- Cloudflare CDN
- Zero Trust tunnels