01
The Vision
Today's Stash is a full stack web application I designed and engineered from the ground up to connect local businesses with consumers through exclusive, time sensitive deals. Think of it as a hyper local "Too Good To Go" but instead of surplus food, it powers promotions for cafes, restaurants, pubs, retail stores, fitness studios, and more across participating towns and regions in Australia.
What started as a concept for helping small business owners drive foot traffic evolved into a production grade SaaS platform with multi role authentication, real time inventory management, AI assisted deal creation, atomic reservation systems, multi channel notifications, and a comprehensive admin panel, all deployed on a modern, scalable infrastructure.
This post walks through the technical architecture, the engineering decisions I made at every layer, and why this platform is built to scale.
02
The Tech Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js (App Router), React 18, TypeScript |
| Styling | Tailwind CSS v4 with custom CSS variables |
| Backend / Database | Supabase (PostgreSQL 15, Auth, Storage, Edge Functions) |
| Authentication | Supabase Auth (Email OTP, Phone OTP via Twilio, Google OAuth, Apple OAuth) |
| SMS Notifications | Twilio Programmable SMS + Twilio Verify |
| Email Notifications | Resend (transactional emails and deal alerts) |
| AI Integration | OpenAI GPT 4o for AI powered deal suggestions |
| Deployment | Vercel (automatic CI/CD from GitHub) |
| Scheduling | PostgreSQL pg_cron for automated background jobs |
Every technology choice was deliberate. Next.js App Router gives me server side rendering, API routes, and file based routing in a single framework. Supabase provides a complete backend as a service with PostgreSQL, real time subscriptions, row level security, and managed auth, all without maintaining my own server infrastructure. Twilio and Resend handle the notification delivery layer, and Vercel provides zero config deployments with edge network performance.
03
Multi Role Architecture
The platform supports three distinct user roles, each with their own permissions, dashboards, and data boundaries:
1. Consumer
Consumers are the end users who browse deals, subscribe to towns, reserve offers, and redeem promotions at local businesses. They have access to a personalised discovery feed, deal detail pages, a reservation management hub, and a full profile and notification settings page.
2. Merchant
Merchants are the business owners who list their venues, set business hours, create and manage deals, and track redemption analytics. They get a dedicated full width dashboard with performance statistics (Total Redemptions, Unique Customers, Estimated Revenue), active and expired deal management, inventory restocking, venue branding (logo and banner upload), QR poster generation, and AI assisted deal creation.
3. Admin
Admins act as platform moderators and regional managers. They can create and manage towns, review merchant applications, monitor the global deals table, manage user reservations and suspensions, and process support requests. Critically, admins operate under a "Sovereign Merchant" model; they cannot create, edit, or delete merchant deals. This ensures data integrity and preserves merchant autonomy over their own pricing and operational data.
Role assignment is enforced at both the database level (via RLS policies) and the application level (via middleware route guards and conditional rendering). Admin roles can only be assigned through the Supabase Dashboard directly, preventing any in app privilege escalation attacks.
04
Authentication and Security
Security is not an afterthought in this application, it is the foundation every feature is built on. Here is how the authentication and authorization system works:
Supabase Auth with Multi Provider Support
The app supports four authentication methods:
- Email OTP Users sign in with a one time password sent to their email, handled natively through Supabase's signInWithOtp and verifyOtp flows.
- Phone OTP (Australian) A custom backend flow (/api/auth/start, /api/auth/verify) calls Twilio Verify directly. All phone numbers are strictly normalised to E.164 Australian format (+614xxxxxxxx) to prevent duplicate accounts and ensure carrier compatibility.
- Google OAuth Fully branded with the app name and logo in the Google Cloud Console OAuth consent screen. The production domain (todaysstash.com.au) is whitelisted in both Google Cloud and Supabase redirect configuration.
- Apple OAuth Integrates with Apple's private relay email system. Resend is configured with the production domain to ensure verification emails reach Apple relay addresses.
Identity Linking and Unlinking
Users who sign up with OAuth can later link a traditional email/password or phone number to their account. Conversely, they can unlink a social account as long as they have an alternative verified login method. This satisfies Supabase's identity integrity constraints while giving users full control over their credentials.
Middleware Level Route Protection
The application uses Next.js middleware (middleware.ts) with @supabase/ssr to intercept every request. The middleware:
- Creates a Supabase server client with cookie based session management.
- Validates the user's session on every route.
- Checks for email confirmation or phone confirmation status.
- Redirects unverified users to a verification page while allowing auth related routes to pass through.
- Uses a mutable response pattern to synchronise refreshed session tokens across the request lifecycle via
setAll().
Row Level Security (RLS)
Every single table in the database has Row Level Security enabled. This means that even if someone managed to bypass the frontend entirely and query the database directly with the anon key, they would only see data they are authorised to access.
Key RLS policies include:
- profiles Users can only read and update their own profile record (user_id = auth.uid()). Admins get full access.
- notification_preferences Restricted to the record owner, with a unique constraint on (user_id, merchant_id) to prevent duplicates.
- applications Anonymous users can INSERT (for the public merchant registration form), but only admins can SELECT.
- verification_codes Fully locked down to admin/service role only. Consumer access is handled through SECURITY DEFINER RPC functions.
- merchants Owners can update their own records (matched through their profile's merchant_id), while admins retain full management access.
Security Definer Functions
All SECURITY DEFINER PostgreSQL functions, including the redemption RPCs, reservation logic, and directory sync triggers, have an explicit search_path set to public, pg_temp. This prevents search path shadowing attacks where a malicious user could create a function in a different schema to intercept queries.
Additional Security Measures
- View Security The legacy public.me security definer view was completely dropped to eliminate critical security warnings. public.success_stories_view uses security_invoker = true to ensure RLS is checked against the calling user.
- Idempotent Migrations All security migrations use the DROP POLICY IF EXISTS / CREATE POLICY pattern, allowing migration scripts to be re run safely.
- Leaked Password Protection Supabase's built in leaked password protection is enabled, blocking users from selecting commonly compromised passwords.
05
The Database Schema
The database is designed around a relational model with carefully normalised tables, JSONB for flexible scheduling data, and integer based pricing for accuracy.
Core Tables
- profiles Extends auth.users with role, subscription plan, subscribed towns (stored as a text[] array for efficient filtering), notification preferences, strike counts, and suspension status.
- towns Geographic areas with image support. Towns can be free or subscription gated.
- merchants Business entities linked to towns, with operating hours stored as JSONB, branding assets (logo and banner URLs), street addresses, and a randomly generated 6 digit merchant PIN for in person redemptions.
- offers The deals themselves. Pricing uses integer cents (price_cents, original_price_cents) to avoid floating point rounding errors, a critical detail for financial accuracy. Scheduling uses a dual system: valid_from/valid_until for campaign level validity and recurring_schedule (JSONB array) for weekly time slots.
- reservations Tracks user commitments with a status lifecycle: pending, redeemed, missed, or cancelled.
- redemptions Records of completed deal uses, linked to both users and optional reservations.
- applications Public merchant lead capture with nullable user_id for anonymous submissions.
- notification_preferences Granular per merchant notification settings with unique constraints and automated timestamp triggers.
Pricing in Cents
A seemingly small but critical design decision: all monetary values are stored as integers in cents. This eliminates the floating point precision issues that plague applications storing prices as decimals. The frontend converts price_cents / 100 for display while never performing arithmetic on floating point dollar values.
06
The Atomic Reservation System
One of the most technically sophisticated features is the reservation system, which I built to prevent race conditions and ensure fair stock allocation.
The Problem
When multiple users try to reserve the last remaining stock of a deal simultaneously, naive implementations can oversell. User A and User B both see "1 left", both click "Reserve", and both get confirmed, but there was only one unit available.
The Solution: reserve_offer RPC with Row Locking
I built a PostgreSQL RPC function (reserve_offer) that uses SELECT ... FOR UPDATE to acquire an exclusive row lock on the specific deal before checking availability:
- 1Authentication Check. Verifies the calling user via auth.uid().
- 2Suspension Check. Queries the profiles table to ensure the user is not suspended.
- 3Daily Limit Enforcement. Checks for existing reservations on the same Sydney timezone calendar day, ignoring cancelled ones.
- 4Row Lock. Locks the offers row with FOR UPDATE to prevent concurrent modifications.
- 5Stock Validation. Verifies redeemed_count < total_limit.
- 6Reservation Creation. Inserts a pending reservation.
- 7Immediate Stock Decrement. Increments redeemed_count at reservation time (not redemption time) to hold the spot.
This entire operation runs inside a single transaction. If any step fails, the entire operation rolls back. No double booking, no overselling.
The Strike System
- No Show Detection The check_missed_reservations() function runs on an hourly pg_cron schedule. It identifies pending reservations where the slot time has passed by more than 12 hours and marks them as missed.
- Strike Issuance Each missed reservation increments profiles.strikes.
- Automatic Suspension At 3 strikes, profiles.is_suspended is set to true, blocking the user from all reservation and redemption activities.
- Admin Override Administrators can lift suspensions via the admin_lift_suspension RPC, which is gated to the admin role and resets both strikes and the suspension flag.
Frontend Implementation
The ReservationModal component uses framework agnostic, React state driven UI instead of native OS selectors. This ensures a consistent experience across iOS, Android, macOS, and Windows:
- Date Selection A pill based button grid showing the next 7 days, filtered to only days that match the deal's recurring_schedule.
- Time Slots Generated in 30 minute increments within the deal's operating window, with past times filtered out for same day bookings.
- 12 Hour Format All times are converted from 24 hour internal format to user friendly 12 hour display.
- Suspension Handling If the RPC fails due to a suspended account, the modal captures the specific error code and displays a user friendly "Account Suspended" message.
07
AI Powered Deal Creation
The merchant dashboard integrates OpenAI's GPT 4o to assist merchants in creating high converting deal listings.
How It Works
- 1The merchant describes their deal in natural language (e.g., "Tuesday morning coffee special" or "Happy hour 4 to 6pm craft beers").
- 2The system sends this to the OpenAI API, which generates three creative deal concepts, each with a marketing optimised title, compelling description, and inferred scheduling data.
- 3The AI processes time related language intelligently: "Mornings" maps to open to 11:00 AM, "Afternoon" maps to 1:00 PM to 5:00 PM, "Evening" maps to 5:00 PM to close. Abstract tokens like open and close are resolved against the merchant’s actual operating hours.
- 4The merchant reviews three consumer aligned previews rendered in a high fidelity CouponPreview component that mirrors the exact styling of the live deal cards.
- 5The merchant selects their preferred concept, adjusts pricing (which the AI never touches; pricing authority stays with the merchant), and publishes.
This "AI as marketing partner, merchant as pricing authority" pattern prevents AI hallucinations in business critical financial data while leveraging the model's strength in creative copywriting and schedule inference.
A manual deal creation flow is always available as a fallback at every stage, ensuring merchants are never locked into the AI workflow.
08
Multi Channel Notification System
The notification system is one of the most complex subsystems in the application, handling SMS delivery via Twilio, email delivery via Resend, carrier level opt out compliance, and granular per merchant subscription management.
Architecture
Users choose exactly one active notification method: Phone (SMS) or Email. Both cannot be active simultaneously. Switching methods requires the new method to be verified first.
Verification Flow
- Phone Uses Twilio Programmable SMS to deliver a 6 digit OTP. The number is stored in pending_phone during verification and only moved to the active phone column after successful code entry. Phone numbers are deduplicated across the entire user base using E.164 normalisation with multiple format variations.
- Email Uses Resend to deliver a 6 digit OTP. Upon successful verification, the system also updates the user's primary email in auth.users via the Supabase Admin API to keep authentication and profile data synchronised.
Deal Alert Broadcasting
When a merchant publishes a new deal or restocks an expired one, the system automatically broadcasts alerts. The API uses a Hybrid Authentication pattern (Bearer token + Cookie fallback) to identify the merchant. An ownership check verifies that the requesting user actually owns the merchant profile. Subscribers are partitioned into SMS and email groups, and messages are dispatched in parallel using Promise.allSettled(), ensuring a failure to deliver to one recipient does not block delivery to the remaining subscribers.
Carrier Compliance (Hard Opt Out)
A production Twilio webhook at /api/webhooks/twilio listens for "STOP" keyword replies. When triggered, it looks up the user's profile by matching the incoming phone number and sets notifications_enabled = false and notification_method = 'none'. This ensures the application's internal state stays synchronised with carrier level blocks, satisfying Australian telecommunications compliance requirements.
Performance Optimisation (N+1 Prevention)
To prevent the N+1 query problem where each deal card on the consumer feed would independently query its notification status, the system uses batch fetching. The parent page fetches all enabled merchant IDs in a single query, stores them in a Set<string> for O(1) lookup, and individual CouponTicket components receive a simple boolean isNotificationEnabled prop, making them stateless for notification logic.
09
The Consumer Experience
TGTG Inspired Design
The consumer UI draws heavy inspiration from the "Too Good To Go" aesthetic: card based discovery with aggressive scarcity indicators, high contrast neon on dark theming, and physical coupon textures.
- Hero Images Fixed 192px height for layout consistency.
- Physical Card Textures Applied as background layers with independent opacity and zoom control via CSS variables.
- Scalloped Edges CSS mask gradient to simulate physical tear off coupons.
- Dynamic Status Badges Strict visual hierarchy: Closed Business > Sold Out > Expired > Active > Starting Today > Tomorrow.
- Smart Countdown Timers 60 second refresh intervals, showing "Expires in 2h 15m" or "Active in 42m" with pulse animations on state changes.
Sophisticated Availability Logic
The getMerchantStatus helper centralises all business hours calculations including overnight shift support (correctly identifies merchants as "Open" when the shift started on the previous calendar day), case insensitive day matching, next open prediction, and Sydney timezone anchoring for consistent timezone handling across the entire deal lifecycle.
Feed Filtering and Sorting
- Near Term Visibility Policy Only shows deals active today or scheduled for tomorrow.
- Dynamic Sort Hierarchy Available Now (Rank 1) > Coming Soon (Rank 2) > Tomorrow (Rank 3) > Sold Out (Rank 4) > Expired (Rank 5) > Closed Business (Rank 10).
- Hybrid Filter UI Persistent "All Deals" and "My Towns" pill tabs with a dropdown for specific town selection.
- Daily Redemption Quotas Each user limited to one redemption per deal per calendar day, resetting at midnight AEST.
10
Merchant Dashboard
The merchant dashboard is a full width, 12 column grid management interface:
- Main Feed (8 columns) Performance stats, active deals with split "Edit" and "Restock" actions, expired deals with a "Re use" template button, and a 50 entry redemption ledger.
- Context Sidebar (4 columns) Venue settings (operating hours, banner upload, address), QR poster generation, and quick actions.
- Inventory Restocking Instead of requiring merchants to calculate total limits manually, the "Restock" feature asks for the desired remaining quantity. The system computes new total_limit = current redemptions + desired remaining, instantly reactivating sold out deals.
- PIN Based and QR Based Redemption Both methods flow through secure RPC functions that validate authentication, check stock limits, link to existing reservations, and record the savings amount.
11
The Admin Panel
The admin panel operates under a "Moderator First" model with strict boundaries:
- Town Management Create and configure geographic areas.
- Application Review Monitor incoming merchant applications with status tracking (Unread, Read, Pending, Approved, Denied).
- Global Deals Table Scheduling aware validity display that calculates and shows "Scheduled" days for recurring deals and "Today Only" status for one off promotions.
- No Deal Backdoors All deal creation and merchant injection code has been excised from the admin panel. The components were physically deleted from the codebase, not just hidden. This ensures absolute sovereignty for merchants over their own data.
12
Supabase Integration
Supabase serves as the entire backend infrastructure. Here is how each service is utilised:
PostgreSQL Database
- 20+ tables with full RLS coverage
- Complex JSONB columns for operating hours
- Integer based pricing for financial accuracy
- Custom ENUM types for roles and status
- Comprehensive migration history
Supabase Auth
- Multi provider authentication
- Identity linking and unlinking
- Cookie based session management
- Production OAuth with domain whitelisting
RPC Functions
- reserve_offer() with row locking
- redeem_offer_with_pin() with auto linking
- check_missed_reservations() via pg_cron
- admin_lift_suspension() role gated
- getUserRole() from profiles and JWT
Storage and Background Jobs
- offer media bucket for deal images
- Randomised filenames to prevent collisions
- Hourly pg_cron execution for strike enforcement
- Automated schema cache management
13
API Architecture
The application uses Next.js API Route Handlers for server side logic that cannot run in the browser:
| Route | Purpose |
|---|---|
| /api/auth/* | Custom Twilio phone authentication flow |
| /api/notifications/send phone verification | Twilio SMS OTP dispatch |
| /api/notifications/send email verification | Resend email OTP dispatch |
| /api/notifications/verify phone / verify email | OTP validation endpoints |
| /api/notifications/update method | Notification method switching |
| /api/notifications/preferences | CRUD for per merchant settings |
| /api/notifications/notify subscribers | Broadcast deal alerts via SMS and email |
| /api/webhooks/twilio | Carrier level opt out compliance webhook |
| /api/auth/reset password/verify | Password reset flow |
All API routes implement the Hybrid Authentication pattern: checking for a Bearer token in the Authorization header first, falling back to cookie based session resolution. This ensures reliable authentication across different client contexts, network latencies, and development environments.
14
The Merchant Onboarding Pipeline
The merchant registration process was intentionally designed as a high touch, manually vetted pipeline:
- 1Public Lead Form (/venue register). Anyone can submit a business application without creating an account. No OTP verification required. RLS allows anonymous inserts.
- 2Admin Review. Applications appear in the admin dashboard as "unread" leads.
- 3Manual Outreach. The sales team contacts applicants offline to verify legitimacy and discuss terms.
- 4Developer Activation. Only after manual approval is the merchant record created and linked to a user account.
- 5Dashboard Access. The business owner logs in to access the full merchant dashboard.
This deliberate friction ensures platform quality and prevents fraudulent listings, a critical differentiator for a consumer facing deals platform.
15
Profile Management and Accessibility
The profile page is designed with a specific focus on accessibility for older users:
- Large text (minimum 16px) and high contrast styling throughout.
- Generous touch targets for all interactive elements.
- Card based method selection instead of tiny radio buttons.
- Inline verification flows with clear step by step progression.
- Name editing with character limits (20 chars), title case enforcement, and number filtering.
- Pre verified identity matching: if a user's profile email matches their authenticated session email, email_verified is automatically set to true.
- Silent background refresh using window.focus event listeners to keep data current without disruptive loading states.
16
Deployment and DevOps
The application follows a streamlined deployment workflow:
- Local development with npm run dev (bindable to 0.0.0.0 for network wide mobile testing).
- Schema sync via npx supabase db push to apply local migrations to production.
- Production build verified locally with npm run build before pushing.
- Git based deployment; pushing to main triggers automatic Vercel builds.
- Environment variable management through Vercel's project settings (Supabase URL/keys, Twilio credentials, Resend API key, app URL).
- Post deployment verification covers authentication flows, data fetching, merchant dashboard access, and notification delivery.
17
What Makes This App Stand Out
True Full Stack Engineering
From PostgreSQL row level security policies and atomic database functions to responsive frontend components with CSS mask gradients and custom state driven selectors. Every layer is hand crafted.
Production Grade Security
RLS on every table, security definer functions with hardened search paths, leaked password protection, carrier level notification compliance, and middleware enforced route protection.
Real World Concurrency Handling
The SELECT ... FOR UPDATE row locking pattern in the reservation system is the same technique used in banking and e commerce systems to prevent race conditions.
AI Integration Done Responsibly
The AI generates creative marketing copy while pricing authority remains firmly with the merchant, preventing hallucinated financial data.
Multi Channel Communication
A unified notification pipeline that handles SMS via Twilio, email via Resend, with automatic carrier opt out compliance and batch subscriber fetching to prevent N+1 queries.
Accessibility First Design
The platform serves a broad demographic including seniors, with large text, generous touch targets, and plain English error messages.
Scalable Architecture
Supabase’s managed PostgreSQL, Vercel’s edge network, and the stateless API design mean this application can scale without re architecture.