Real-time event social feeds
Product

CenterStage: A Social Interaction Event Tool

Built with Next.js, TypeScript, Supabase, and Stripe, CenterStage is a SaaS platform that transforms live events by putting guests at the center of the experience. Attendees scan a QR code, share photos, videos, and comments from their phones — no app download required — and their content appears on screen in real time during the event. Designed for corporate conferences, association events, and social celebrations, CenterStage gives planners a turnkey engagement tool that enhances the guest experience without disrupting the main event.

The Mission

One of the marketing pages that demonstrate how the application works to potential subscribers

One of the marketing pages that demonstrate how the application works to potential subscribers

Working with my long-time event planning client, Animatic Media, CenterStage was born from a simple idea: give event organizers a way to bring their audience into the moment. Whether it's a wedding reception, a corporate conference, or a community fundraiser, the platform lets guests scan a QR code, submit photos, videos, and comments from their phones, and see that content appear on a live display in the room — no app download required. My job was to architect and deliver the full-stack SaaS product from the ground up: authentication, multi-tenant data isolation, subscription billing, an admin dashboard, a real-time presentation engine, and a marketing site.

Tech Stack

The application is built on Next.js 16 with the App Router and React 19, written entirely in TypeScript. The UI layer combines Material UI 7 for the admin dashboard with Tailwind CSS 4 for the marketing pages and presentation views. Data lives in Supabase (PostgreSQL), payments run through Stripe, transactional email is handled by Postmark, and the whole thing is deployed on Vercel. Tests are written with Jest 30 and achieve 90%+ coverage on the core business logic layer.

Authentication Engine

Rather than lean on a third-party auth provider, I built a custom credential-based authentication system on top of NextAuth.js v5. Passwords are hashed with bcryptjs using a cost factor of 10. The system supports four distinct user roles — super_admin, account_owner, admin, and viewer — each with different permission boundaries enforced at the database query level and through Next.js middleware.

I built out a fully function role-based admin system for project and account management.

I built out a fully function role-based admin system for project and account management.

Account owners register through a signup flow that creates both a user record and an account record in a single transaction, with automatic rollback if either step fails. From there, the owner can invite team members via email. Each invitation generates a unique token, and the invited user completes registration through a token-validated flow that links them to the correct account. Email confirmation uses a separate token lifecycle: on signup, users receive a confirmation email, and the system tracks is_email_confirmed status to gate certain actions.

Session management uses JWT-based tokens with custom claims that embed the user's role, account ID, and display name. The middleware layer inspects these claims on every request to enforce route-level access control — admin routes are locked to authenticated users with the appropriate role, and API routes validate both authentication and authorization before touching any data.

Multi-Tenant Architecture and Database Layer

Every query in the system is scoped to an account. The database schema enforces multi-tenant isolation through account_id foreign keys on projects, users, and subscriptions. I wrote a dedicated data access layer — over 70 functions across modules for accounts, projects, submissions, users, subscriptions, and webhook events — that wraps all Supabase queries with consistent error handling and account-scoped filtering.

The super_admin role is a special case: these users bypass account scoping and can see all data across the platform, which powers the internal admin tools for managing subscriptions, overriding billing, and troubleshooting customer issues.

Projects are the core organizational unit. Each project belongs to an account and contains its own configuration: display settings, branding, fonts, colors, auto-approve rules, and content filtering. When a project is created, a default configuration record is generated alongside it. Deletion is a multi-step operation that cleans up associated storage assets (uploaded photos and videos in Supabase Storage) before removing the database records.

Subscription and Billing System

The billing system was one of the most complex parts of the build. CenterStage offers three subscription tiers — Essential, Professional, and Premium — each available in monthly, quarterly, and annual cadences. On top of that, users can purchase one-off project credits that extend their project limits without upgrading their plan.

Configured multiple subscription and pricing tiers for end-users to subscribe to.

Configured multiple subscription and pricing tiers for end-users to subscribe to.

On the Stripe side, I integrated the full lifecycle: Checkout Sessions for initial purchase, Customer Portal for self-service management, subscription upgrades and downgrades with prorated billing, and cancellation with reactivation support. The system reuses Stripe customer IDs across sessions so returning customers don't create duplicate records.

Webhook processing is where the real complexity lives. The application handles events, including checkout.session.completed, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted. Each webhook event is logged with an idempotency check — if the same event ID arrives twice, it's silently ignored (handled via PostgreSQL's unique constraint error code 23505). A cleanup job prunes processed webhook records older than 90 days.

The admin dashboard exposes manual override controls for subscriptions. A super admin can set any account to any tier, bypassing Stripe entirely, with a reason field and audit trail (override_by_user_id, override_at, override_reason). This proved essential for handling edge cases like comp accounts, beta testers, and billing disputes.

Usage tracking ties into the subscription system: each subscription has an associated usage record that tracks projects_created_count, projects_live_count, and one_off_projects_purchased. Tier limits are defined in a central configuration that maps each tier to its allowed project count, admin seats, and feature flags. The system checks these limits before allowing project creation and surfaces clear upgrade prompts when a user hits their ceiling.

Admin Dashboard and Project Configuration

Each project can configure the look and feel of their projects, as well as control the comment animations on the presentation screen.

Each project can configure the look and feel of their projects, as well as control the comment animations on the presentation screen.

The admin interface is built with Material UI and provides full CRUD management for projects, submissions, users, and subscriptions. Projects support an extensive set of configuration options that control the presentation experience:

  • Display settings: cycle speed, animation style (slide, fade, zoom, flip, or collage mode), background color or uploaded background image, content scaling, and padding
  • Typography: 16+ Google Fonts available, with configurable font family, size, color, and weight — exposed through CSS custom properties that the presentation engine consumes at runtime
  • Branding: custom logo upload, QR code generation for the submission page, and a public-facing submission URL with a unique project slug
  • Moderation: submissions can be auto-approved or held for manual review. When auto-approve is enabled, an integrated content filter scans submissions for inappropriate language using word-boundary matching and leet-speak detection before allowing them through
  • Submission display modes: each submission can be set to display once, repeat, or pin to the rotation, with optional custom timing overrides

Presentation Engine

User's posts are presented on a screen in real-time, for an emersive social interaction

User's posts are presented on a screen in real-time, for an emersive social interaction

The public-facing display is a standalone view designed to run full-screen on a TV or projector. It cycles through approved submissions using the configured animation style and timing. The collage mode arranges multiple submissions on screen simultaneously with randomized positioning. All display parameters — fonts, colors, speeds, animations — are pulled from the project configuration at render time, making each event's display fully customizable without code changes.

Email System

Transactional email runs through Postmark with 10 distinct templates covering the full user lifecycle: welcome emails, email confirmation, password reset, team invitations, subscription confirmations, payment failure notices, cancellation confirmations, reactivation confirmations, and trial-ending reminders. Each template is a pure function that accepts dynamic data and returns HTML wrapped in a shared base layout with consistent branding. In development, email sending is disabled when the API key is absent, and all email content is logged to the console instead.

Content Moderation and Security

Beyond the content filter, the submission flow includes reCAPTCHA v3 integration with configurable score thresholds and action validation. File uploads are validated for size limits, MIME types, and file extensions before being stored in Supabase Storage with unique, collision-resistant paths. Rate limiting is implemented at the API level with configurable windows and request caps, using IP detection that respects X-Forwarded-For, X-Real-IP, and CF-Connecting-IP headers for accurate client identification behind proxies and CDNs.

Reflections

CenterStage pushed me to think carefully about multi-tenant data isolation, subscription state machines, and the realities of browser security policies (autoplay restrictions alone took several iterations to get right). The project reinforced my preference for building focused data access layers over raw ORM queries, and for writing comprehensive tests — the 1,000+ test suite caught real bugs during every major refactor. Shipping a product that handles real payments, real user data, and real-time display for live events demanded a level of rigor that made me a stronger engineer.