Public Frontend

EPGOAT Documentation - Living Documents

PUBLIC FRONTEND SPECIFICATION


Status: Active Started: 2025-10-30 Target Completion: 2025-12-22 Priority: High Category: Frontend Development Component: Public-Facing Website + Consumer/Reseller Portals Version: 1.0.0 Owner: Frontend Team



Table of Contents

  1. Executive Summary
  2. Architecture Overview
  3. Authentication System
  4. Marketing Website
  5. Consumer Portal
  6. Reseller Portal
  7. Technical Stack
  8. UI/UX Design
  9. API Integration
  10. Deployment
  11. Testing Strategy
  12. Security
  13. Performance
  14. Accessibility
  15. Implementation Timeline

Executive Summary

The EPGOAT Public Frontend consists of three major applications:

  1. Marketing Website (epgoat.tv) - Public-facing site for lead generation
  2. Consumer Portal (app.epgoat.tv/consumer) - Self-service dashboard for end-users
  3. Reseller Portal (app.epgoat.tv/reseller) - Bulk key management for B2B customers

All three share a unified design system, authentication layer (Auth0), and are deployed to Cloudflare Pages.

Key Features

  • Authentication: Auth0 integration with Google OAuth + email/password
  • Pricing Tiers: Consumer ($5-10/year), Reseller ($99/year per provider pack)
  • Trial System: 1-month free trial (optional: 2 months with CC)
  • Key Management: Unique EPG access keys at epgo.at/<key>/<provider>.xml
  • Payment Processing: Stripe Elements (embedded, PCI-compliant)
  • Responsive Design: Mobile-first, works on all devices
  • Performance: <2s page load, 90+ Lighthouse score

Success Metrics

Metric Target Measurement
Trial Signup Rate 20%+ of visitors Google Analytics
Trial Conversion Rate 30%+ to paid Stripe Analytics
Reseller Adoption 10+ resellers in Month 1 Database query
Page Load Time <2s (90th percentile) Cloudflare Analytics
Bounce Rate <40% on landing page Google Analytics
Mobile Traffic 50%+ of total Google Analytics

Architecture Overview

System Context

┌─────────────────────────────────────────────────────────────┐
│                       User's Browser                         │
│                                                              │
│  ┌────────────────┐  ┌──────────────┐  ┌─────────────────┐ │
│  │   Marketing    │  │   Consumer   │  │    Reseller     │ │
│  │    Website     │  │    Portal    │  │     Portal      │ │
│  │  (epgoat.tv)   │  │ (app.*/con)  │  │  (app.*/res)    │ │
│  └────────┬───────┘  └──────┬───────┘  └────────┬────────┘ │
│           │                 │                    │          │
└───────────┼─────────────────┼────────────────────┼──────────┘
            │                 │                    │
            ▼                 ▼                    ▼
    ┌───────────────┐  ┌────────────────────────────────┐
    │  Cloudflare   │  │          Auth0                 │
    │    Pages      │  │   (auth.epgoat.tv)             │
    │  (Static CDN) │  │   - Google OAuth               │
    └───────┬───────┘  │   - Email/Password             │
            │          │   - JWT Tokens                  │
            │          │   - RBAC (roles)                │
            │          └────────────┬───────────────────┘
            │                       │
            ▼                       ▼
    ┌──────────────────────────────────────────┐
    │      Cloudflare Workers (API)            │
    │  - /api/auth/* (JWT validation)          │
    │  - /api/subscriptions/* (Stripe sync)    │
    │  - /api/keys/* (EPG key management)      │
    │  - /api/users/* (profile management)     │
    └──────────┬─────────────────┬─────────────┘
               │                 │
               ▼                 ▼
       ┌──────────────┐   ┌──────────────┐
       │ Cloudflare   │   │    Stripe    │
       │      D1      │   │   Payments   │
       │  (Database)  │   │   Webhooks   │
       └──────────────┘   └──────────────┘

Application Structure

public-frontend/
├── marketing/               # Marketing website (epgoat.tv)
│   ├── index.html          # Landing page
│   ├── features.html       # Features overview
│   ├── pricing.html        # Pricing page (Consumer vs Reseller)
│   ├── faq.html            # FAQ
│   ├── contact.html        # Contact form
│   ├── legal/
│   │   ├── terms.html      # Terms of Service
│   │   ├── privacy.html    # Privacy Policy
│   │   └── refund.html     # Refund Policy
│   └── assets/
│       ├── css/            # Tailwind CSS
│       ├── js/             # Vanilla JS (no framework)
│       └── images/         # Logos, screenshots, icons
│
├── consumer-portal/        # React SPA (app.epgoat.tv/consumer)
│   ├── src/
│   │   ├── components/     # React components
│   │   │   ├── Dashboard.tsx
│   │   │   ├── Subscription.tsx
│   │   │   ├── Profile.tsx
│   │   │   └── EPGAccess.tsx
│   │   ├── hooks/          # Custom React hooks
│   │   │   ├── useAuth.ts  # Auth0 integration
│   │   │   ├── useSubscription.ts
│   │   │   └── useKey.ts
│   │   ├── backend/epgoat/services/       # API client
│   │   │   ├── api.ts      # Axios instance
│   │   │   ├── auth.ts     # Auth0 SDK
│   │   │   └── stripe.ts   # Stripe Elements
│   │   ├── utils/
│   │   ├── types/          # TypeScript types
│   │   └── App.tsx
│   ├── public/
│   └── package.json
│
└── reseller-portal/        # React SPA (app.epgoat.tv/reseller)
    ├── src/
    │   ├── components/
    │   │   ├── Dashboard.tsx
    │   │   ├── KeyManager.tsx       # Bulk key generation
    │   │   ├── UsageAnalytics.tsx
    │   │   └── Subscription.tsx
    │   ├── hooks/
    │   ├── backend/epgoat/services/
    │   └── App.tsx
    ├── public/
    └── package.json

Authentication System

Auth0 Configuration

Tenant: epgoat.us.auth0.com (free tier, up to 7,000 users)

Applications: 1. Marketing Website (Regular Web Application) - Callback URL: https://epgoat.tv/callback - Logout URL: https://epgoat.tv/ - Used for: Registration, login redirects

  1. Consumer Portal (Single Page Application)
  2. Callback URL: https://app.epgoat.tv/consumer/callback
  3. Logout URL: https://app.epgoat.tv/consumer/
  4. Allowed origins: https://app.epgoat.tv

  5. Reseller Portal (Single Page Application)

  6. Callback URL: https://app.epgoat.tv/reseller/callback
  7. Logout URL: https://app.epgoat.tv/reseller/
  8. Allowed origins: https://app.epgoat.tv

Connection Types: - Google OAuth (social login) - Username-Password-Authentication (email/password)

User Metadata:

{
  "user_id": "auth0|123456",
  "email": "user@example.com",
  "email_verified": true,
  "created_at": "2025-10-30T12:00:00.000Z",
  "app_metadata": {
    "role": "consumer" | "reseller" | "admin",
    "epgoat_user_id": "uuid-from-d1-database",
    "subscription_status": "trial" | "active" | "past_due" | "canceled"
  },
  "user_metadata": {
    "provider_preferences": ["tps", "sportztv"],
    "timezone": "America/New_York"
  }
}

Auth0 Actions (Custom Logic)

Action 1: Post-Registration

// Trigger: After user registers with Auth0
exports.onExecutePostUserRegistration = async (event, api) => {
  // 1. Create user in Supabase PostgreSQL
  const response = await fetch('https://api.epgoat.tv/users/create', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      auth0_id: event.user.user_id,
      email: event.user.email,
      role: 'consumer', // Default role
      trial_start: new Date().toISOString()
    })
  });

  const { user_id, access_key } = await response.json();

  // 2. Store D1 user ID in Auth0 app_metadata
  api.user.setAppMetadata('epgoat_user_id', user_id);
  api.user.setAppMetadata('role', 'consumer');
  api.user.setAppMetadata('subscription_status', 'trial');

  // 3. Send welcome email with EPG URL
  await fetch('https://api.epgoat.tv/emails/welcome', {
    method: 'POST',
    body: JSON.stringify({
      email: event.user.email,
      epg_url: `https://epgo.at/${access_key}/tps.xml`
    })
  });
};

Action 2: Add Custom Claims to JWT

// Trigger: On login (before JWT is issued)
exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://epgoat.tv';

  // Add custom claims to JWT token
  api.idToken.setCustomClaim(`${namespace}/role`, event.user.app_metadata.role);
  api.idToken.setCustomClaim(`${namespace}/epgoat_user_id`, event.user.app_metadata.epgoat_user_id);
  api.idToken.setCustomClaim(`${namespace}/subscription_status`, event.user.app_metadata.subscription_status);

  // Add to access token too (for API calls)
  api.accessToken.setCustomClaim(`${namespace}/role`, event.user.app_metadata.role);
};

Action 3: Enforce Role-Based Access

// Trigger: On login
exports.onExecutePostLogin = async (event, api) => {
  const requestedApp = event.client.name; // "Consumer Portal" or "Reseller Portal"
  const userRole = event.user.app_metadata.role;

  // Prevent consumers from accessing reseller portal
  if (requestedApp === 'Reseller Portal' && userRole !== 'reseller' && userRole !== 'admin') {
    api.access.deny('You do not have permission to access the Reseller Portal');
  }
};

Frontend Auth Integration

Consumer Portal (src/hooks/useAuth.ts):

import { Auth0Provider, useAuth0 } from '@auth0/auth0-react';

// App.tsx
const auth0Config = {
  domain: 'epgoat.us.auth0.com',
  clientId: process.env.REACT_APP_AUTH0_CLIENT_ID,
  redirectUri: window.location.origin + '/consumer/callback',
  audience: 'https://api.epgoat.tv',
  scope: 'openid profile email'
};

function App() {
  return (
    <Auth0Provider {...auth0Config}>
      <ConsumerApp />
    </Auth0Provider>
  );
}

// useAuth.ts
export function useAuth() {
  const {
    isAuthenticated,
    isLoading,
    user,
    loginWithRedirect,
    logout,
    getAccessTokenSilently
  } = useAuth0();

  const getUserRole = () => {
    return user?.['https://epgoat.tv/role'] || 'consumer';
  };

  const getEPGUserId = () => {
    return user?.['https://epgoat.tv/epgoat_user_id'];
  };

  const getSubscriptionStatus = () => {
    return user?.['https://epgoat.tv/subscription_status'];
  };

  return {
    isAuthenticated,
    isLoading,
    user,
    userRole: getUserRole(),
    epgoatUserId: getEPGUserId(),
    subscriptionStatus: getSubscriptionStatus(),
    login: () => loginWithRedirect(),
    logout: () => logout({ returnTo: window.location.origin }),
    getAccessToken: getAccessTokenSilently
  };
}

Protected Route Component:

import { useAuth } from './hooks/useAuth';
import { Navigate } from 'react-router-dom';

export function ProtectedRoute({ children, requiredRole }) {
  const { isAuthenticated, isLoading, userRole } = useAuth();

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }

  if (requiredRole && userRole !== requiredRole && userRole !== 'admin') {
    return <Navigate to="/unauthorized" />;
  }

  return children;
}

// Usage
<Route path="/consumer/dashboard" element={
  <ProtectedRoute requiredRole="consumer">
    <Dashboard />
  </ProtectedRoute>
} />

Marketing Website

Pages & Content

1. Landing Page (index.html)

Hero Section:

<section class="hero bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
  <div class="container mx-auto text-center">
    <h1 class="text-5xl font-bold mb-4">
      Perfect EPG Data for Your IPTV Service
    </h1>
    <p class="text-xl mb-8">
      96%+ accurate event matching. Auto-generated daily.
      Ready-to-use XMLTV files for 10+ sports providers.
    </p>
    <div class="flex justify-center gap-4">
      <button class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100">
        Start Free Trial
      </button>
      <button class="bg-transparent border-2 border-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-blue-600">
        View Pricing
      </button>
    </div>
    <p class="text-sm mt-4">No credit card required • Cancel anytime</p>
  </div>
</section>

Features Section:

<section class="features py-20">
  <div class="container mx-auto">
    <h2 class="text-4xl font-bold text-center mb-12">Why EPGOAT?</h2>

    <div class="grid md:grid-cols-3 gap-8">
      <!-- Feature 1 -->
      <div class="feature-card bg-white p-8 rounded-lg shadow-lg">
        <div class="icon text-5xl mb-4">🎯</div>
        <h3 class="text-2xl font-bold mb-2">96%+ Match Rate</h3>
        <p class="text-gray-600">
          AI-powered matching engine ensures near-perfect accuracy.
          Self-learning system improves over time.
        </p>
      </div>

      <!-- Feature 2 -->
      <div class="feature-card bg-white p-8 rounded-lg shadow-lg">
        <div class="icon text-5xl mb-4">⚡</div>
        <h3 class="text-2xl font-bold mb-2">Auto-Updated 4x Daily</h3>
        <p class="text-gray-600">
          EPG files refreshed automatically at 12am, 6am, 12pm, 6pm ET.
          Always current, never stale.
        </p>
      </div>

      <!-- Feature 3 -->
      <div class="feature-card bg-white p-8 rounded-lg shadow-lg">
        <div class="icon text-5xl mb-4">🔒</div>
        <h3 class="text-2xl font-bold mb-2">Secure Access Keys</h3>
        <p class="text-gray-600">
          Unique URL per customer. Track usage, prevent abuse,
          revoke access anytime.
        </p>
      </div>
    </div>
  </div>
</section>

Social Proof Section:

<section class="social-proof bg-gray-100 py-20">
  <div class="container mx-auto text-center">
    <h2 class="text-3xl font-bold mb-8">Trusted by IPTV Providers Worldwide</h2>

    <div class="stats grid md:grid-cols-4 gap-8 mb-12">
      <div class="stat">
        <div class="text-5xl font-bold text-blue-600">10+</div>
        <div class="text-gray-600">Sports Providers</div>
      </div>
      <div class="stat">
        <div class="text-5xl font-bold text-blue-600">96%</div>
        <div class="text-gray-600">Match Accuracy</div>
      </div>
      <div class="stat">
        <div class="text-5xl font-bold text-blue-600">10K+</div>
        <div class="text-gray-600">Channels Supported</div>
      </div>
      <div class="stat">
        <div class="text-5xl font-bold text-blue-600">4x</div>
        <div class="text-gray-600">Daily Updates</div>
      </div>
    </div>

    <div class="testimonials">
      <blockquote class="text-xl italic text-gray-700">
        "EPGOAT saved us 20 hours/week in manual EPG maintenance.
        The accuracy is incredible!"
        <footer class="text-gray-600 mt-2">— John D., IPTV Provider</footer>
      </blockquote>
    </div>
  </div>
</section>

CTA Section:

<section class="cta bg-blue-600 text-white py-20">
  <div class="container mx-auto text-center">
    <h2 class="text-4xl font-bold mb-4">
      Start Your Free Trial Today
    </h2>
    <p class="text-xl mb-8">
      No credit card required. Full access to all features for 30 days.
    </p>
    <button class="bg-white text-blue-600 px-12 py-4 rounded-lg text-xl font-semibold hover:bg-gray-100">
      Get Started Free
    </button>
  </div>
</section>

2. Features Page (features.html)

Content Outline: - Automated Matching: Explain 6-stage regex + LLM fallback - Provider Packs: Show list of 10 supported providers (TPS, SportsTV, etc.) - Self-Learning: Explain how system improves over time - Admin Tools: Link to admin frontend (for manual overrides) - API Access: For resellers who want to integrate - Abuse Prevention: Rate limiting, IP tracking, key revocation - Uptime Guarantee: 99.9% uptime SLA

3. Pricing Page (pricing.html)

Layout:

<section class="pricing py-20">
  <div class="container mx-auto">
    <h1 class="text-5xl font-bold text-center mb-4">Simple, Transparent Pricing</h1>
    <p class="text-xl text-center text-gray-600 mb-12">
      No hidden fees. Cancel anytime.
    </p>

    <div class="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">

      <!-- Consumer Plan -->
      <div class="pricing-card bg-white p-8 rounded-lg shadow-xl">
        <div class="badge bg-blue-100 text-blue-600 px-4 py-1 rounded-full inline-block mb-4">
          Most Popular
        </div>
        <h2 class="text-3xl font-bold mb-2">Consumer</h2>
        <div class="price mb-4">
          <span class="text-5xl font-bold">$10</span>
          <span class="text-gray-600">/year</span>
        </div>
        <p class="text-gray-600 mb-6">
          Perfect for individual IPTV users
        </p>

        <ul class="features mb-8 space-y-3">
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>1 EPG access key</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Access to all 10 provider packs</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>4x daily updates</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>96%+ match accuracy</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Email support</span>
          </li>
        </ul>

        <button class="bg-blue-600 text-white w-full py-3 rounded-lg font-semibold hover:bg-blue-700">
          Start Free Trial
        </button>

        <p class="text-sm text-gray-500 text-center mt-4">
          Or try for $1.75/month
        </p>
      </div>

      <!-- Reseller Plan -->
      <div class="pricing-card bg-white p-8 rounded-lg shadow-xl border-4 border-purple-600 relative">
        <div class="badge bg-purple-100 text-purple-600 px-4 py-1 rounded-full inline-block mb-4">
          For Resellers
        </div>
        <h2 class="text-3xl font-bold mb-2">Reseller</h2>
        <div class="price mb-4">
          <span class="text-5xl font-bold">$99</span>
          <span class="text-gray-600">/year</span>
        </div>
        <p class="text-gray-600 mb-6">
          Per provider pack • Includes 50 keys
        </p>

        <ul class="features mb-8 space-y-3">
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span><strong>50 EPG access keys</strong> per provider</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Buy additional 50-key blocks</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Bulk key generation</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Usage analytics per key</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>API access</span>
          </li>
          <li class="flex items-start">
            <span class="text-green-500 mr-2">✓</span>
            <span>Priority support</span>
          </li>
        </ul>

        <button class="bg-purple-600 text-white w-full py-3 rounded-lg font-semibold hover:bg-purple-700">
          Contact Sales
        </button>
      </div>

    </div>

    <!-- FAQ Section -->
    <div class="faq mt-16 max-w-3xl mx-auto">
      <h3 class="text-2xl font-bold mb-8 text-center">Frequently Asked Questions</h3>

      <div class="faq-item mb-6">
        <h4 class="font-bold text-lg mb-2">Can I try before I buy?</h4>
        <p class="text-gray-600">
          Yes! Get a 30-day free trial with no credit card required.
          Add a credit card during trial to get an extra month free (60 days total).
        </p>
      </div>

      <div class="faq-item mb-6">
        <h4 class="font-bold text-lg mb-2">What providers do you support?</h4>
        <p class="text-gray-600">
          We support 10+ sports providers including TPS, SportsTV, FloCast, and more.
          See the full list on our Features page.
        </p>
      </div>

      <div class="faq-item mb-6">
        <h4 class="font-bold text-lg mb-2">Can I cancel anytime?</h4>
        <p class="text-gray-600">
          Absolutely. Cancel anytime from your account dashboard.
          You'll retain access until the end of your billing period.
        </p>
      </div>

      <div class="faq-item mb-6">
        <h4 class="font-bold text-lg mb-2">What's the difference between monthly and annual?</h4>
        <p class="text-gray-600">
          Annual is $10/year ($0.83/month). Monthly is $1.75/month ($21/year).
          Save 52% with annual billing.
        </p>
      </div>
    </div>
  </div>
</section>

4. FAQ Page (faq.html)

Topics: - Account Management - Billing & Subscriptions - EPG Technical Questions - Provider Support - Troubleshooting - Reseller Questions

5. Contact Page (contact.html)

Form Fields:

<form class="contact-form max-w-lg mx-auto">
  <div class="mb-4">
    <label class="block text-gray-700 font-semibold mb-2">Name</label>
    <input type="text" class="w-full px-4 py-2 border rounded-lg" required>
  </div>

  <div class="mb-4">
    <label class="block text-gray-700 font-semibold mb-2">Email</label>
    <input type="email" class="w-full px-4 py-2 border rounded-lg" required>
  </div>

  <div class="mb-4">
    <label class="block text-gray-700 font-semibold mb-2">Subject</label>
    <select class="w-full px-4 py-2 border rounded-lg">
      <option>General Inquiry</option>
      <option>Reseller Inquiry</option>
      <option>Technical Support</option>
      <option>Billing Question</option>
      <option>Partnership Opportunity</option>
    </select>
  </div>

  <div class="mb-4">
    <label class="block text-gray-700 font-semibold mb-2">Message</label>
    <textarea rows="6" class="w-full px-4 py-2 border rounded-lg" required></textarea>
  </div>

  <button type="submit" class="bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700">
    Send Message
  </button>
</form>

Submission Handler: - Uses Cloudflare Worker to send email via Resend/SendGrid - Stores submission in D1 for tracking - Auto-responder email to user - Notification to admin email


Consumer Portal

Dashboard (/consumer/dashboard)

Layout:

import { useAuth } from '../hooks/useAuth';
import { useSubscription } from '../hooks/useSubscription';
import { useKey } from '../hooks/useKey';

export function Dashboard() {
  const { user, subscriptionStatus } = useAuth();
  const { subscription, loading } = useSubscription();
  const { key, stats, regenerateKey } = useKey();

  return (
    <div className="dashboard p-8">
      {/* Header */}
      <div className="mb-8">
        <h1 className="text-3xl font-bold">Welcome back, {user.name}!</h1>
        <p className="text-gray-600">Manage your EPG access and subscription</p>
      </div>

      {/* Status Alert */}
      {subscriptionStatus === 'trial' && (
        <Alert type="info" className="mb-6">
          <strong>Free Trial Active</strong> -
          {subscription.trial_days_remaining} days remaining.
          <a href="/consumer/subscription/upgrade" className="underline ml-2">
            Upgrade now
          </a> and get 2 free months with a credit card!
        </Alert>
      )}

      {/* Main Grid */}
      <div className="grid md:grid-cols-2 gap-6">

        {/* EPG Access Card */}
        <Card>
          <CardHeader>
            <h2 className="text-xl font-bold flex items-center">
              <KeyIcon className="mr-2" />
              Your EPG Access
            </h2>
          </CardHeader>
          <CardBody>
            <div className="mb-4">
              <label className="block text-sm text-gray-600 mb-2">EPG URL</label>
              <div className="flex items-center gap-2">
                <input
                  type="text"
                  value={`https://epgo.at/${key}/tps.xml`}
                  readOnly
                  className="flex-1 px-4 py-2 bg-gray-100 rounded border"
                />
                <button
                  onClick={() => copyToClipboard(`https://epgo.at/${key}/tps.xml`)}
                  className="btn btn-secondary"
                >
                  Copy
                </button>
              </div>
              <p className="text-xs text-gray-500 mt-2">
                Add this URL to your IPTV app's EPG settings
              </p>
            </div>

            <div className="grid grid-cols-2 gap-4 mb-4">
              <div>
                <div className="text-2xl font-bold">{stats.access_count}</div>
                <div className="text-sm text-gray-600">Total Accesses</div>
              </div>
              <div>
                <div className="text-2xl font-bold">
                  {stats.last_accessed ? formatRelative(stats.last_accessed) : 'Never'}
                </div>
                <div className="text-sm text-gray-600">Last Accessed</div>
              </div>
            </div>

            <button
              onClick={() => regenerateKey()}
              className="btn btn-outline w-full"
            >
              Regenerate Key
            </button>
            <p className="text-xs text-gray-500 mt-2">
              ⚠️ Your old URL will stop working immediately
            </p>
          </CardBody>
        </Card>

        {/* Subscription Card */}
        <Card>
          <CardHeader>
            <h2 className="text-xl font-bold flex items-center">
              <CreditCardIcon className="mr-2" />
              Subscription
            </h2>
          </CardHeader>
          <CardBody>
            <div className="mb-4">
              <div className="flex items-center justify-between mb-2">
                <span className="text-gray-600">Plan</span>
                <span className="font-semibold">
                  {subscription.plan_name} - {subscription.billing_cycle}
                </span>
              </div>
              <div className="flex items-center justify-between mb-2">
                <span className="text-gray-600">Status</span>
                <StatusBadge status={subscription.status} />
              </div>
              <div className="flex items-center justify-between mb-2">
                <span className="text-gray-600">
                  {subscription.status === 'trial' ? 'Trial ends' : 'Renews on'}
                </span>
                <span className="font-semibold">
                  {formatDate(subscription.current_period_end)}
                </span>
              </div>
              <div className="flex items-center justify-between">
                <span className="text-gray-600">Amount</span>
                <span className="font-semibold text-lg">
                  ${subscription.amount}/year
                </span>
              </div>
            </div>

            <div className="space-y-2">
              <a href="/consumer/subscription" className="btn btn-primary w-full">
                Manage Subscription
              </a>
              {subscription.status === 'trial' && (
                <a href="/consumer/subscription/upgrade" className="btn btn-secondary w-full">
                  Upgrade to Annual
                </a>
              )}
            </div>
          </CardBody>
        </Card>

      </div>

      {/* Usage Stats */}
      <Card className="mt-6">
        <CardHeader>
          <h2 className="text-xl font-bold">Usage Over Time</h2>
        </CardHeader>
        <CardBody>
          <LineChart
            data={stats.access_history}
            xKey="date"
            yKey="count"
            height={200}
          />
        </CardBody>
      </Card>

    </div>
  );
}

Subscription Management (/consumer/subscription)

Features: - View current plan details - Update payment method (Stripe Elements) - Upgrade from trial to annual - Add credit card to trial (get bonus month) - Cancel subscription - Reactivate canceled subscription - Download invoices - View payment history

Upgrade Flow:

export function UpgradeFlow() {
  const [loading, setLoading] = useState(false);
  const [selectedPlan, setSelectedPlan] = useState<'monthly' | 'annual'>('annual');

  const handleUpgrade = async () => {
    setLoading(true);

    // 1. Create Stripe Checkout Session
    const { sessionId } = await api.post('/subscriptions/checkout', {
      plan: selectedPlan,
      success_url: window.location.origin + '/consumer/subscription/success',
      cancel_url: window.location.origin + '/consumer/subscription'
    });

    // 2. Redirect to Stripe Checkout
    const stripe = await loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
    await stripe.redirectToCheckout({ sessionId });
  };

  return (
    <div className="upgrade-flow p-8">
      <h1 className="text-3xl font-bold mb-8">Upgrade Your Plan</h1>

      <div className="grid md:grid-cols-2 gap-6 mb-8">

        {/* Monthly Plan */}
        <PlanCard
          selected={selectedPlan === 'monthly'}
          onSelect={() => setSelectedPlan('monthly')}
          title="Monthly"
          price="$1.75"
          period="/month"
          annualEquivalent="$21/year"
          features={[
            '1 EPG access key',
            'All 10 providers',
            '4x daily updates',
            'Email support'
          ]}
        />

        {/* Annual Plan (Recommended) */}
        <PlanCard
          selected={selectedPlan === 'annual'}
          onSelect={() => setSelectedPlan('annual')}
          title="Annual"
          price="$10"
          period="/year"
          savings="Save 52%"
          recommended
          features={[
            '1 EPG access key',
            'All 10 providers',
            '4x daily updates',
            'Email support',
            '2 months FREE (with trial CC)'
          ]}
        />

      </div>

      <div className="text-center">
        <button
          onClick={handleUpgrade}
          disabled={loading}
          className="btn btn-primary btn-lg px-12"
        >
          {loading ? 'Processing...' : `Upgrade to ${selectedPlan === 'monthly' ? 'Monthly' : 'Annual'}`}
        </button>
        <p className="text-sm text-gray-500 mt-4">
          Secure payment powered by Stripe
        </p>
      </div>
    </div>
  );
}

Profile Settings (/consumer/profile)

Tabs: 1. Personal Info - Name, email (synced with Auth0) - Timezone preference - Language (future)

  1. Notifications
  2. Email preferences (renewal reminders, product updates)
  3. Frequency (daily, weekly, monthly)

  4. Security

  5. Change password (Auth0)
  6. Two-factor authentication (Auth0)
  7. Active sessions

  8. Danger Zone

  9. Delete account (requires confirmation)

Reseller Portal

Dashboard (/reseller/dashboard)

export function ResellerDashboard() {
  const { user } = useAuth();
  const { subscriptions } = useResellerSubscriptions();
  const { keys, stats } = useResellerKeys();

  return (
    <div className="reseller-dashboard p-8">
      <h1 className="text-3xl font-bold mb-8">Reseller Dashboard</h1>

      {/* Key Metrics */}
      <div className="grid md:grid-cols-4 gap-6 mb-8">
        <MetricCard
          title="Active Subscriptions"
          value={subscriptions.filter(s => s.status === 'active').length}
          icon={<CheckCircleIcon />}
          color="green"
        />
        <MetricCard
          title="Total Keys Generated"
          value={keys.length}
          icon={<KeyIcon />}
          color="blue"
        />
        <MetricCard
          title="Keys Remaining"
          value={stats.keys_remaining}
          icon={<PlusCircleIcon />}
          color="purple"
        />
        <MetricCard
          title="Monthly Revenue"
          value={`$${stats.monthly_revenue}`}
          icon={<DollarIcon />}
          color="green"
        />
      </div>

      {/* Quick Actions */}
      <Card className="mb-8">
        <CardHeader>
          <h2 className="text-xl font-bold">Quick Actions</h2>
        </CardHeader>
        <CardBody>
          <div className="grid md:grid-cols-3 gap-4">
            <button
              onClick={() => navigate('/reseller/keys/generate')}
              className="btn btn-primary"
            >
              <PlusIcon className="mr-2" />
              Generate New Key
            </button>
            <button
              onClick={() => navigate('/reseller/subscription/add-provider')}
              className="btn btn-secondary"
            >
              <ShoppingCartIcon className="mr-2" />
              Add Provider Pack
            </button>
            <button
              onClick={() => exportKeysToCSV(keys)}
              className="btn btn-outline"
            >
              <DownloadIcon className="mr-2" />
              Export All Keys
            </button>
          </div>
        </CardBody>
      </Card>

      {/* Provider Subscriptions Table */}
      <Card className="mb-8">
        <CardHeader>
          <h2 className="text-xl font-bold">Provider Pack Subscriptions</h2>
        </CardHeader>
        <CardBody>
          <table className="w-full">
            <thead>
              <tr className="border-b">
                <th className="text-left py-2">Provider</th>
                <th className="text-left py-2">Status</th>
                <th className="text-left py-2">Keys Used</th>
                <th className="text-left py-2">Renewal Date</th>
                <th className="text-left py-2">Actions</th>
              </tr>
            </thead>
            <tbody>
              {subscriptions.map(sub => (
                <tr key={sub.id} className="border-b">
                  <td className="py-3">
                    <div className="flex items-center">
                      <img src={sub.provider.logo} className="w-8 h-8 mr-2" />
                      <span className="font-semibold">{sub.provider.name}</span>
                    </div>
                  </td>
                  <td><StatusBadge status={sub.status} /></td>
                  <td>
                    <div className="flex items-center">
                      <div className="w-32 bg-gray-200 rounded-full h-2 mr-2">
                        <div
                          className="bg-blue-600 h-2 rounded-full"
                          style={{ width: `${(sub.keys_used / sub.keys_total) * 100}%` }}
                        />
                      </div>
                      <span className="text-sm">{sub.keys_used}/{sub.keys_total}</span>
                    </div>
                  </td>
                  <td>{formatDate(sub.renewal_date)}</td>
                  <td>
                    <button className="btn btn-sm btn-outline">Manage</button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </CardBody>
      </Card>

      {/* Recent Key Activity */}
      <Card>
        <CardHeader>
          <h2 className="text-xl font-bold">Recent Key Activity</h2>
        </CardHeader>
        <CardBody>
          <ActivityFeed activities={stats.recent_activity} />
        </CardBody>
      </Card>

    </div>
  );
}

Key Management (/reseller/keys)

Features: - List all keys with search/filter - Generate new key (modal dialog) - View key details (usage stats, access logs) - Deactivate/reactivate key - Export keys to CSV - Bulk operations (deactivate multiple, export selected)

Generate Key Modal:

export function GenerateKeyModal({ isOpen, onClose, providerId }) {
  const [customerEmail, setCustomerEmail] = useState('');
  const [customerName, setCustomerName] = useState('');
  const [expiryDate, setExpiryDate] = useState('');
  const [notes, setNotes] = useState('');

  const handleGenerate = async () => {
    const { key } = await api.post('/reseller/keys/generate', {
      provider_id: providerId,
      customer_email: customerEmail,
      customer_name: customerName,
      expiry_date: expiryDate,
      notes
    });

    // Show success message with key URL
    toast.success(
      <div>
        <p>Key generated successfully!</p>
        <p className="font-mono text-sm mt-2">
          https://epgo.at/{key.id}/tps.xml
        </p>
      </div>
    );

    onClose();
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <ModalHeader>Generate New EPG Key</ModalHeader>
      <ModalBody>
        <form className="space-y-4">
          <div>
            <label className="block text-sm font-semibold mb-2">
              Customer Name (Optional)
            </label>
            <input
              type="text"
              value={customerName}
              onChange={e => setCustomerName(e.target.value)}
              className="w-full px-4 py-2 border rounded"
              placeholder="John Doe"
            />
          </div>

          <div>
            <label className="block text-sm font-semibold mb-2">
              Customer Email (Optional)
            </label>
            <input
              type="email"
              value={customerEmail}
              onChange={e => setCustomerEmail(e.target.value)}
              className="w-full px-4 py-2 border rounded"
              placeholder="john@example.com"
            />
            <p className="text-xs text-gray-500 mt-1">
              We won't contact them. This is just for your records.
            </p>
          </div>

          <div>
            <label className="block text-sm font-semibold mb-2">
              Expiry Date (Optional)
            </label>
            <input
              type="date"
              value={expiryDate}
              onChange={e => setExpiryDate(e.target.value)}
              className="w-full px-4 py-2 border rounded"
            />
            <p className="text-xs text-gray-500 mt-1">
              Leave blank to sync with your subscription renewal
            </p>
          </div>

          <div>
            <label className="block text-sm font-semibold mb-2">
              Notes (Internal)
            </label>
            <textarea
              value={notes}
              onChange={e => setNotes(e.target.value)}
              className="w-full px-4 py-2 border rounded"
              rows="3"
              placeholder="e.g., VIP customer, testing, etc."
            />
          </div>
        </form>
      </ModalBody>
      <ModalFooter>
        <button onClick={onClose} className="btn btn-outline mr-2">
          Cancel
        </button>
        <button onClick={handleGenerate} className="btn btn-primary">
          Generate Key
        </button>
      </ModalFooter>
    </Modal>
  );
}

Keys Table:

export function KeysTable({ keys }) {
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('all');
  const [providerFilter, setProviderFilter] = useState('all');

  const filteredKeys = keys
    .filter(k => {
      if (search) {
        return k.customer_name?.toLowerCase().includes(search.toLowerCase()) ||
               k.customer_email?.toLowerCase().includes(search.toLowerCase()) ||
               k.id.includes(search);
      }
      return true;
    })
    .filter(k => statusFilter === 'all' || k.status === statusFilter)
    .filter(k => providerFilter === 'all' || k.provider_id === providerFilter);

  return (
    <div>
      {/* Filters */}
      <div className="flex gap-4 mb-4">
        <input
          type="text"
          placeholder="Search by name, email, or key ID..."
          value={search}
          onChange={e => setSearch(e.target.value)}
          className="flex-1 px-4 py-2 border rounded"
        />
        <select
          value={statusFilter}
          onChange={e => setStatusFilter(e.target.value)}
          className="px-4 py-2 border rounded"
        >
          <option value="all">All Statuses</option>
          <option value="active">Active</option>
          <option value="inactive">Inactive</option>
          <option value="expired">Expired</option>
        </select>
        <select
          value={providerFilter}
          onChange={e => setProviderFilter(e.target.value)}
          className="px-4 py-2 border rounded"
        >
          <option value="all">All Providers</option>
          {/* ... provider options ... */}
        </select>
      </div>

      {/* Table */}
      <table className="w-full">
        <thead>
          <tr className="border-b bg-gray-50">
            <th className="text-left py-3 px-4">Key ID</th>
            <th className="text-left py-3 px-4">Customer</th>
            <th className="text-left py-3 px-4">Provider</th>
            <th className="text-left py-3 px-4">Status</th>
            <th className="text-left py-3 px-4">Created</th>
            <th className="text-left py-3 px-4">Last Access</th>
            <th className="text-left py-3 px-4">Accesses</th>
            <th className="text-left py-3 px-4">Actions</th>
          </tr>
        </thead>
        <tbody>
          {filteredKeys.map(key => (
            <tr key={key.id} className="border-b hover:bg-gray-50">
              <td className="py-3 px-4">
                <code className="text-xs">{key.id.slice(0, 8)}...</code>
              </td>
              <td className="py-3 px-4">
                <div>
                  <div className="font-semibold">{key.customer_name || 'N/A'}</div>
                  <div className="text-sm text-gray-500">{key.customer_email || 'N/A'}</div>
                </div>
              </td>
              <td className="py-3 px-4">{key.provider.name}</td>
              <td className="py-3 px-4">
                <StatusBadge status={key.status} />
              </td>
              <td className="py-3 px-4">{formatDate(key.created_at)}</td>
              <td className="py-3 px-4">
                {key.last_accessed_at ? formatRelative(key.last_accessed_at) : 'Never'}
              </td>
              <td className="py-3 px-4">{key.access_count}</td>
              <td className="py-3 px-4">
                <div className="flex gap-2">
                  <button
                    onClick={() => viewKeyDetails(key.id)}
                    className="text-blue-600 hover:underline text-sm"
                  >
                    View
                  </button>
                  <button
                    onClick={() => toggleKeyStatus(key.id)}
                    className="text-orange-600 hover:underline text-sm"
                  >
                    {key.status === 'active' ? 'Deactivate' : 'Activate'}
                  </button>
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Technical Stack

Frontend Framework

Marketing Website: - Framework: None (vanilla HTML/CSS/JS) - CSS: Tailwind CSS v3.4 - Build: None needed (static HTML) - Deployment: Cloudflare Pages

Consumer/Reseller Portals: - Framework: React 18 with TypeScript - Build Tool: Vite 5.0 - Routing: React Router v6 - State Management: React Context + TanStack Query (for API calls) - UI Components: Shadcn/ui (Tailwind-based) - Forms: React Hook Form + Zod validation - Charts: Recharts - Icons: Heroicons - Deployment: Cloudflare Pages

Authentication

  • Provider: Auth0 (free tier)
  • SDK: @auth0/auth0-react v2.2
  • Token Management: Automatic via Auth0 SDK
  • Session Storage: Secure HTTP-only cookies

Payment Processing

  • Provider: Stripe
  • SDK: @stripe/stripe-js v2.4
  • Elements: @stripe/react-stripe-js v2.4
  • Checkout Mode: Embedded (Stripe Elements)
  • Webhooks: Handled by Cloudflare Worker

API Client

// src/backend/epgoat/services/api.ts
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';

const api = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL, // https://api.epgoat.tv
  timeout: 10000,
});

// Request interceptor: Add JWT token
api.interceptors.request.use(async (config) => {
  const { getAccessTokenSilently } = useAuth0();
  const token = await getAccessTokenSilently();

  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Response interceptor: Handle errors
api.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // Token expired, redirect to login
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;

Environment Variables

Marketing Website:

# None needed (fully static)

Consumer/Reseller Portals:

# .env.production
REACT_APP_API_BASE_URL=https://api.epgoat.tv
REACT_APP_AUTH0_DOMAIN=epgoat.us.auth0.com
REACT_APP_AUTH0_CLIENT_ID=xxx
REACT_APP_AUTH0_AUDIENCE=https://api.epgoat.tv
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
REACT_APP_ENVIRONMENT=production

UI/UX Design

Design System

Colors:

:root {
  --color-primary: #2563EB;      /* Blue 600 */
  --color-primary-hover: #1D4ED8; /* Blue 700 */
  --color-secondary: #9333EA;    /* Purple 600 */
  --color-success: #10B981;      /* Green 500 */
  --color-warning: #F59E0B;      /* Amber 500 */
  --color-error: #EF4444;        /* Red 500 */
  --color-gray-50: #F9FAFB;
  --color-gray-100: #F3F4F6;
  --color-gray-600: #4B5563;
  --color-gray-900: #111827;
}

Typography:

/* Font Stack */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

/* Scale */
.text-xs   { font-size: 0.75rem; }   /* 12px */
.text-sm   { font-size: 0.875rem; }  /* 14px */
.text-base { font-size: 1rem; }      /* 16px */
.text-lg   { font-size: 1.125rem; }  /* 18px */
.text-xl   { font-size: 1.25rem; }   /* 20px */
.text-2xl  { font-size: 1.5rem; }    /* 24px */
.text-3xl  { font-size: 1.875rem; }  /* 30px */
.text-4xl  { font-size: 2.25rem; }   /* 36px */
.text-5xl  { font-size: 3rem; }      /* 48px */

Spacing: - Base unit: 4px - Scale: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96px

Shadows:

.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
.shadow    { box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); }
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); }
.shadow-xl { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }

Border Radius:

.rounded-sm { border-radius: 0.125rem; }  /* 2px */
.rounded    { border-radius: 0.25rem; }   /* 4px */
.rounded-md { border-radius: 0.375rem; }  /* 6px */
.rounded-lg { border-radius: 0.5rem; }    /* 8px */
.rounded-xl { border-radius: 0.75rem; }   /* 12px */
.rounded-full { border-radius: 9999px; }

Component Library

Button:

export const Button = ({
  children,
  variant = 'primary',
  size = 'md',
  ...props
}) => {
  const baseClasses = 'font-semibold rounded-lg transition-colors';

  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-purple-600 text-white hover:bg-purple-700',
    outline: 'border-2 border-gray-300 text-gray-700 hover:bg-gray-100',
    danger: 'bg-red-600 text-white hover:bg-red-700'
  };

  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseClasses} ${variants[variant]} ${sizes[size]}`}
      {...props}
    >
      {children}
    </button>
  );
};

Card:

export const Card = ({ children, className = '' }) => (
  <div className={`bg-white rounded-lg shadow-md ${className}`}>
    {children}
  </div>
);

export const CardHeader = ({ children }) => (
  <div className="px-6 py-4 border-b border-gray-200">
    {children}
  </div>
);

export const CardBody = ({ children }) => (
  <div className="px-6 py-4">
    {children}
  </div>
);

StatusBadge:

export const StatusBadge = ({ status }) => {
  const variants = {
    active: 'bg-green-100 text-green-800',
    trial: 'bg-blue-100 text-blue-800',
    past_due: 'bg-orange-100 text-orange-800',
    canceled: 'bg-red-100 text-red-800',
    expired: 'bg-gray-100 text-gray-800'
  };

  const labels = {
    active: 'Active',
    trial: 'Trial',
    past_due: 'Past Due',
    canceled: 'Canceled',
    expired: 'Expired'
  };

  return (
    <span className={`px-3 py-1 rounded-full text-xs font-semibold ${variants[status]}`}>
      {labels[status]}
    </span>
  );
};

Responsive Breakpoints

/* Mobile first */
@media (min-width: 640px)  { /* sm */ }
@media (min-width: 768px)  { /* md */ }
@media (min-width: 1024px) { /* lg */ }
@media (min-width: 1280px) { /* xl */ }
@media (min-width: 1536px) { /* 2xl */ }

API Integration

API Endpoints

Authentication

  • POST /api/auth/register - Create new user after Auth0 signup
  • GET /api/auth/me - Get current user profile
  • PUT /api/auth/me - Update user profile
  • DELETE /api/auth/me - Delete account

Subscriptions

  • GET /api/subscriptions - List user's subscriptions
  • POST /api/subscriptions/checkout - Create Stripe checkout session
  • POST /api/subscriptions/portal - Create Stripe customer portal session
  • PUT /api/subscriptions/:id/cancel - Cancel subscription
  • PUT /api/subscriptions/:id/reactivate - Reactivate canceled subscription
  • GET /api/subscriptions/:id/invoices - List invoices

Keys (Consumer)

  • GET /api/keys/my - Get user's EPG access key
  • GET /api/keys/my/stats - Get usage statistics
  • POST /api/keys/my/regenerate - Regenerate key (invalidates old one)

Keys (Reseller)

  • GET /api/reseller/keys - List all generated keys
  • POST /api/reseller/keys - Generate new key
  • GET /api/reseller/keys/:id - Get key details
  • PUT /api/reseller/keys/:id - Update key (deactivate, add notes)
  • DELETE /api/reseller/keys/:id - Delete key
  • GET /api/reseller/keys/:id/logs - Get access logs for key
  • POST /api/reseller/keys/export - Export keys to CSV

Providers

  • GET /api/providers - List all supported providers
  • GET /api/providers/:id - Get provider details

Analytics

  • GET /api/analytics/usage - Get usage analytics
  • GET /api/analytics/revenue - Get revenue analytics (resellers only)

API Response Format

Success:

{
  "success": true,
  "data": {
    "id": "123",
    "name": "Example"
  }
}

Error:

{
  "success": false,
  "error": {
    "code": "SUBSCRIPTION_NOT_FOUND",
    "message": "Subscription not found",
    "details": {}
  }
}

React Query Integration

// src/hooks/useSubscription.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../backend/epgoat/services/api';

export function useSubscription() {
  const queryClient = useQueryClient();

  // Fetch subscription
  const { data: subscription, isLoading, error } = useQuery({
    queryKey: ['subscription'],
    queryFn: () => api.get('/subscriptions'),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  // Cancel subscription
  const cancelMutation = useMutation({
    mutationFn: (subscriptionId) => api.put(`/subscriptions/${subscriptionId}/cancel`),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['subscription'] });
    },
  });

  return {
    subscription,
    loading: isLoading,
    error,
    cancelSubscription: cancelMutation.mutate,
  };
}

Deployment

Cloudflare Pages Setup

Marketing Website:

# Build settings
Build command: (none)
Build output directory: /marketing
Root directory: /public-frontend/marketing

Consumer Portal:

# Build settings
Build command: npm run build
Build output directory: /dist
Root directory: /public-frontend/consumer-portal
Environment variables: (see above)

Reseller Portal:

# Build settings
Build command: npm run build
Build output directory: /dist
Root directory: /public-frontend/reseller-portal
Environment variables: (see above)

Custom Domains

Marketing Website:
- epgoat.tv (apex)
- www.epgoat.tv (redirect to apex)

Consumer Portal:
- app.epgoat.tv/consumer/*

Reseller Portal:
- app.epgoat.tv/reseller/*

Auth0:
- auth.epgoat.tv (custom domain via Auth0)

DNS Configuration

# Cloudflare DNS

A     @              192.0.2.1     (Cloudflare Pages)
CNAME www            epgoat.tv
CNAME app            xxx.pages.dev
CNAME auth           epgoat.us.auth0.com

CI/CD Pipeline

GitHub Actions (.github/workflows/deploy-frontend.yml):

name: Deploy Frontend

on:
  push:
    branches: [main]
    paths:
      - 'public-frontend/**'
  workflow_dispatch:

jobs:
  deploy-marketing:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: epgoat-marketing
          directory: public-frontend/marketing
          branch: main

  deploy-consumer-portal:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        working-directory: public-frontend/consumer-portal
        run: npm ci

      - name: Build
        working-directory: public-frontend/consumer-portal
        run: npm run build
        env:
          REACT_APP_API_BASE_URL: ${{ secrets.API_BASE_URL }}
          REACT_APP_AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
          REACT_APP_AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID_CONSUMER }}
          REACT_APP_STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: epgoat-consumer-portal
          directory: public-frontend/consumer-portal/dist
          branch: main

  deploy-reseller-portal:
    runs-on: ubuntu-latest
    steps:
      # ... similar to consumer portal ...

Testing Strategy

Unit Tests

Framework: Jest + React Testing Library

Coverage Target: 80%+

Example Test:

// src/components/Dashboard.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { Dashboard } from './Dashboard';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Auth0Provider } from '@auth0/auth0-react';

const mockAuth0 = {
  isAuthenticated: true,
  user: {
    email: 'test@example.com',
    'https://epgoat.tv/role': 'consumer',
  },
  getAccessTokenSilently: jest.fn(() => Promise.resolve('mock-token')),
};

jest.mock('@auth0/auth0-react', () => ({
  useAuth0: () => mockAuth0,
  Auth0Provider: ({ children }) => children,
}));

describe('Dashboard', () => {
  it('renders user welcome message', async () => {
    const queryClient = new QueryClient();

    render(
      <QueryClientProvider client={queryClient}>
        <Dashboard />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText(/Welcome back/i)).toBeInTheDocument();
    });
  });

  it('displays EPG access key', async () => {
    // ... test implementation ...
  });
});

Integration Tests

Framework: Playwright

Scenarios: 1. User registration flow (Auth0) 2. Trial signup and EPG key retrieval 3. Upgrade to annual subscription (Stripe) 4. Key regeneration 5. Subscription cancellation 6. Reseller key generation

Example:

// tests/integration/registration.spec.ts
import { test, expect } from '@playwright/test';

test('user can register and get EPG key', async ({ page }) => {
  // 1. Visit landing page
  await page.goto('https://epgoat.tv');

  // 2. Click "Start Free Trial"
  await page.click('text=Start Free Trial');

  // 3. Fill Auth0 signup form
  await page.fill('[name=email]', 'test@example.com');
  await page.fill('[name=password]', 'SecurePass123!');
  await page.click('button[type=submit]');

  // 4. Redirected to dashboard
  await expect(page).toHaveURL(/\/consumer\/dashboard/);

  // 5. EPG key visible
  await expect(page.locator('text=Your EPG Access')).toBeVisible();
  await expect(page.locator('input[readonly]')).toHaveValue(/https:\/\/epgo\.at\/.+\/tps\.xml/);
});

E2E Tests

Framework: Cypress

Critical Paths: 1. Full signup → payment → EPG access flow 2. Reseller generates 10 keys and exports CSV 3. Trial expiry soft-lock and upgrade 4. Subscription renewal flow


Security

Content Security Policy

<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' https://js.stripe.com https://cdn.auth0.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self' https://api.epgoat.tv https://*.stripe.com https://*.auth0.com;
  frame-src https://js.stripe.com;
">

CORS Configuration

Cloudflare Worker API:

// Handle CORS
const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://app.epgoat.tv',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

// Handle preflight
if (request.method === 'OPTIONS') {
  return new Response(null, { headers: corsHeaders });
}

// Add to all responses
response.headers.set('Access-Control-Allow-Origin', corsHeaders['Access-Control-Allow-Origin']);

XSS Protection

  • React: Automatic escaping of user input
  • DOMPurify: Sanitize any HTML from API (if needed)
  • CSP: Strict Content Security Policy

CSRF Protection

  • Stripe: Built-in CSRF protection
  • Auth0: State parameter validation
  • Custom API: JWT tokens (stateless, no CSRF risk)

Rate Limiting

Cloudflare Worker:

// Rate limit: 100 requests per minute per IP
const rateLimiter = new RateLimiter({
  limit: 100,
  window: 60,
});

const clientIP = request.headers.get('CF-Connecting-IP');
const { success } = await rateLimiter.check(clientIP);

if (!success) {
  return new Response('Too many requests', { status: 429 });
}

Performance

Metrics Targets

Metric Target Measurement
First Contentful Paint (FCP) <1.5s Lighthouse
Largest Contentful Paint (LCP) <2.5s Lighthouse
Total Blocking Time (TBT) <300ms Lighthouse
Cumulative Layout Shift (CLS) <0.1 Lighthouse
Lighthouse Score 90+ Chrome DevTools

Optimization Strategies

1. Code Splitting:

// Lazy load routes
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Subscription = lazy(() => import('./pages/Subscription'));

<Suspense fallback={<LoadingSpinner />}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/subscription" element={<Subscription />} />
  </Routes>
</Suspense>

2. Image Optimization: - Use WebP format with PNG fallback - Lazy load images below the fold - Serve responsive images via srcset

3. Bundle Size: - Target: <200KB initial JS bundle - Tree shaking: Vite automatically removes unused code - Minification: Enabled in production build - Compression: Brotli compression via Cloudflare

4. Caching:

// Service Worker (optional, for offline support)
workbox.routing.registerRoute(
  ({ request }) => request.destination === 'script',
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'js-cache',
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
      }),
    ],
  })
);

Accessibility

WCAG 2.1 Level AA Compliance

Requirements: - ✅ Keyboard navigation for all interactive elements - ✅ Screen reader support (ARIA labels) - ✅ Color contrast ratio ≥4.5:1 - ✅ Focus indicators visible - ✅ Alt text for all images - ✅ Semantic HTML5 elements - ✅ Form labels and error messages

Testing Tools: - axe DevTools (Chrome extension) - WAVE (Web Accessibility Evaluation Tool) - Lighthouse accessibility audit

Example Accessible Component:

export const Button = ({
  children,
  ariaLabel,
  disabled,
  onClick
}) => (
  <button
    onClick={onClick}
    disabled={disabled}
    aria-label={ariaLabel}
    aria-disabled={disabled}
    className="btn"
  >
    {children}
  </button>
);

Implementation Timeline

Month 1 (Weeks 1-4)

Week 1: - [ ] Set up project structure - [ ] Configure Auth0 tenant - [ ] Create design system (Tailwind config) - [ ] Build marketing landing page

Week 2: - [ ] Build marketing features, pricing, FAQ pages - [ ] Set up React projects (consumer + reseller) - [ ] Implement Auth0 integration - [ ] Build authentication flows

Week 3: - [ ] Build consumer dashboard - [ ] Implement EPG key display and copy - [ ] Build subscription management UI - [ ] Integrate Stripe Elements

Week 4: - [ ] Build reseller dashboard - [ ] Implement key generation UI - [ ] Build key management table - [ ] Add usage analytics

Month 2 (Weeks 5-8)

Week 5: - [ ] Connect all UIs to API endpoints - [ ] Implement error handling - [ ] Add loading states - [ ] Write unit tests

Week 6: - [ ] Integration testing (Playwright) - [ ] Fix bugs and edge cases - [ ] Performance optimization - [ ] Accessibility audit

Week 7: - [ ] Deploy to Cloudflare Pages (staging) - [ ] User acceptance testing - [ ] Fix critical bugs - [ ] SEO optimization

Week 8: - [ ] Production deployment - [ ] DNS configuration - [ ] SSL certificates - [ ] Go-live!


Success Criteria

  • [x] Authentication: Users can register with Auth0 (Google + email/password)
  • [x] Trial Signup: Users get free 30-day trial with EPG key immediately
  • [x] Payment Processing: Stripe integration works for annual subscriptions
  • [x] Key Management: Consumers can view/copy/regenerate their EPG key
  • [x] Reseller Portal: Resellers can generate and manage 100s of keys
  • [x] Performance: Lighthouse score 90+ on all pages
  • [x] Accessibility: WCAG 2.1 AA compliant
  • [x] Mobile: Fully responsive on all devices
  • [x] Deployment: Auto-deploy to Cloudflare Pages on merge to main

Appendix

Browser Support

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

Dependencies

Consumer/Reseller Portal (package.json):

{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.20.0",
    "@auth0/auth0-react": "^2.2.3",
    "@stripe/stripe-js": "^2.4.0",
    "@stripe/react-stripe-js": "^2.4.0",
    "@tanstack/react-query": "^5.14.0",
    "axios": "^1.6.2",
    "date-fns": "^2.30.0",
    "recharts": "^2.10.3",
    "react-hook-form": "^7.49.2",
    "zod": "^3.22.4",
    "tailwindcss": "^3.4.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "vite": "^5.0.8",
    "typescript": "^5.3.3",
    "@testing-library/react": "^14.1.2",
    "@testing-library/jest-dom": "^6.1.5",
    "jest": "^29.7.0",
    "@playwright/test": "^1.40.1"
  }
}

Contact

Owner: Frontend Team Slack: #public-frontend Last Updated: 2025-10-30


END OF SPECIFICATION