Skip to main content

Overview

BoxApp is a modern, multi-tenant CrossFit gym management SaaS platform built with React, TypeScript, and Supabase. The application follows a provider-based architecture with strict tenant isolation and role-based access control.

Technology Stack

Frontend

  • Framework: React 18 with TypeScript
  • Build Tool: Vite
  • Routing: React Router v6
  • State Management: React Context API
  • UI Components: Custom components with Tailwind CSS
  • Theming: Dynamic theming system with per-tenant customization

Backend

  • Database: PostgreSQL via Supabase
  • Authentication: Supabase Auth (email/password + OAuth)
  • API: Supabase PostgREST (auto-generated REST API)
  • Real-time: Supabase Realtime subscriptions
  • Storage: Supabase Storage for avatars and assets

Security

  • Authorization: PostgreSQL Row-Level Security (RLS)
  • Multi-tenancy: Box-scoped data isolation
  • Audit System: Automatic change tracking via triggers

Application Architecture

Provider Hierarchy

The application uses a nested provider architecture to manage global state:
// src/App.tsx:252-265
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
  <TenantProvider>
    <BrowserRouter>
      <AuthProviderWithTenant>
        <AppContent />
      </AuthProviderWithTenant>
    </BrowserRouter>
  </TenantProvider>
</ThemeProvider>
The provider order is critical: TenantProvider must wrap AuthProvider because authentication depends on tenant context (box_id).

Context Providers

1. TenantProvider

Resolves the tenant (box) from the URL before authentication:
// src/contexts/TenantContext.tsx:8-15
interface TenantContextType {
    tenantSlug: string | null;
    tenantBox: BoxRow | null;
    isTenantSubdomain: boolean;  // true if getTenantSlug() !== null
    isSuspended: boolean;         // true if subscription_status is 'suspended' or 'cancelled'
    tenantNotFound: boolean;      // true if the slug doesn't exist in the DB
    isLoading: boolean;
}
Responsibilities:
  • Extract tenant slug from hostname (prod) or query param (dev)
  • Fetch box configuration from database
  • Determine if tenant is suspended
  • Provide tenant context to child components

2. AuthProvider

Manages user authentication and profile with tenant awareness:
// src/contexts/AuthContext.tsx:9-27
interface AuthContextType {
    session: Session | null;
    user: User | null;
    userProfile: Profile | null;
    currentBox: Box | null;
    loading: boolean;
    isAdmin: boolean;
    isCoach: boolean;
    isRoot: boolean;
    isAthlete: boolean;
    signIn: (credentials: any) => Promise<{ error: any; data?: any }>;
    signInWithGoogle: (boxId?: string) => Promise<{ error: any }>;
    signUp: (credentials: any) => Promise<{ data: any; error: any }>;
    resetPassword: (email: string) => Promise<{ error: any }>;
    updateUser: (attributes: any) => Promise<{ data: any; error: any }>;
    signOut: () => Promise<void>;
    refreshProfile: () => Promise<void>;
    setCurrentBox: (box: Box | null) => void;
}
Key Features:
  • Accepts tenantBoxId prop for multi-tenant context
  • Automatically reconciles box_id in user profile
  • Fetches box settings after authentication
  • Provides role-based flags (isAdmin, isCoach, etc.)

3. ThemeProvider

Manages dynamic theming with per-tenant customization:
// src/App.tsx:69-93
useEffect(() => {
    if (currentBox) {
        // Update Branding (Title & Favicon)
        const baseTitle = 'BoxApp';
        const boxName = currentBox.name || 'CrossFit Management';
        document.title = `${boxName} | ${baseTitle}`;

        if (currentBox.favicon_url) {
            let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
            if (!link) {
                link = document.createElement('link');
                link.rel = 'icon';
                document.getElementsByTagName('head')[0].appendChild(link);
            }
            link.href = currentBox.favicon_url;
        }

        // Update Theme Config
        if (currentBox.theme_config) {
            const config = currentBox.theme_config as any;
            if (config.primaryColor) setPrimaryColor(config.primaryColor);
            if (config.radius) setRadius(config.radius);
            if (config.designStyle) setDesignStyle(config.designStyle);
        }
    }
}, [currentBox, setPrimaryColor, setRadius, setDesignStyle]);

Routing Structure

Public Routes

Accessible without authentication:
  • /login - Main login page
  • /auth/callback - OAuth callback handler
  • /register - Self-service box registration

Protected Routes

Require authentication and role-based permissions:
// src/App.tsx:165-232
<Route path="/members"
  element={
    <ProtectedRoute allowedRoles={['admin', 'coach', 'receptionist']}>
      <Members userProfile={userProfile} />
    </ProtectedRoute>
  }
/>
RouteAllowed RolesDescription
/dashboardAll authenticatedMain dashboard
/scheduleAll authenticatedClass schedule
/membersadmin, coach, receptionistMember management
/leadsadmin, receptionistLead tracking
/wodsAll authenticatedWOD management
/analyticsadminAnalytics dashboard
/settingsAll authenticatedUser settings
/billingadmin, receptionistBilling management
/rolesadminRole management
/audit-logsadminAudit log viewer
/benchmarksAll authenticatedBenchmark tracking
/competitionsadmin, coachCompetition management
/movementsadmin, coachMovement library
/profileAll authenticatedUser profile
/adminroot onlySuper admin panel

Component Organization

Layout Components

  • MainLayout: Wraps authenticated pages with navigation and sidebar
  • ProtectedRoute: HOC for role-based route protection

Loading States

The application has multiple loading phases:
  1. Tenant Resolution (TenantProvider)
  2. Authentication (AuthProvider)
  3. Profile Fetch (after auth)
  4. Box Settings (after profile)
// src/App.tsx:96-114
// 1. Wait for tenant resolution (slug → box fetch)
if (tenantLoading) {
    return <LoadingSpinner />;
}

// 2. Unknown slug — show friendly error instead of blank screen
if (tenantNotFound) {
    return <TenantNotFoundScreen />;
}

// 3. Suspended tenant — block ALL access, including authenticated users
if (isSuspended) {
    return <SuspendedScreen />;
}

// 4. Wait for auth session resolution
if (loading) {
    return <LoadingSpinner />;
}
The loading sequence is critical. Rendering routes before tenant/auth resolution can cause security bypasses or data leaks.

Configuration

Environment Variables

// src/lib/supabaseClient.ts:4-5
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
Required environment variables:
  • VITE_SUPABASE_URL: Supabase project URL
  • VITE_SUPABASE_ANON_KEY: Supabase anonymous/public key

Supabase Client

// src/lib/supabaseClient.ts:14
export const supabase = createClient<Database>(supabaseUrl || '', supabaseAnonKey || '');
The client is typed with the generated Database type from Supabase.

Super Admin System

BoxApp includes a super admin panel for platform management:
// src/App.tsx:145-147
const isSuperAdmin =
    session?.user?.email === '[email protected]' ||
    session?.user?.user_metadata?.is_root === true;
Super admins bypass tenant isolation and can manage all boxes across the platform.

Data Flow

Authentication Flow

  1. User visits tenant URL (e.g., crossfitbox.boxora.website)
  2. TenantProvider extracts slug and fetches box data
  3. User submits login credentials
  4. AuthProvider.signIn() calls Supabase Auth
  5. On success, fetch user profile with box_id
  6. Reconcile box_id from tenant context if needed
  7. Fetch box settings and apply theme
  8. Render authenticated routes

OAuth Flow

// src/contexts/AuthContext.tsx:169-185
const signInWithGoogle = async (boxId?: string) => {
    console.log('[AuthContext] Google OAuth started, boxId:', boxId);

    // Store box context in localStorage so fetchProfile can reconcile it after redirect
    if (boxId || tenantBoxId) {
        localStorage.setItem('pending_box_id', boxId || (tenantBoxId as string));
    }

    const result = await supabase.auth.signInWithOAuth({
        provider: 'google',
        options: {
            redirectTo: `${window.location.origin}/auth/callback`,
        },
    });
    // onAuthStateChange will handle session + profile fetch after redirect
    return { error: result.error };
};
The pending_box_id localStorage key ensures the user is associated with the correct box after OAuth redirect.

Error Handling

Tenant Not Found

Shows a friendly error when the tenant slug doesn’t exist:
// src/App.tsx:33-51
const TenantNotFoundScreen: React.FC = () => (
  <div className="min-h-screen bg-[#050508] text-white flex items-center justify-center p-4">
    <div className="text-center space-y-4 max-w-sm">
      <div className="h-14 w-14 mx-auto rounded-2xl bg-white/[0.04] border border-white/[0.08] flex items-center justify-center">
        <span className="text-2xl">🔍</span>
      </div>
      <h1 className="text-xl font-bold">Box no encontrado</h1>
      <p className="text-sm text-white/40">
        Este box no existe o ha sido eliminado.
      </p>
      <a
        href="/register"
        className="inline-block text-sm text-white/50 hover:text-white transition-colors mt-2"
      >
        ¿Quieres registrar tu box? →
      </a>
    </div>
  </div>
);

Suspended Tenant

Blocks all access when subscription is suspended or cancelled:
// src/App.tsx:106-109
if (isSuspended) {
    return <SuspendedScreen />;
}

Performance Considerations

Loading Optimization

The application implements safety timeouts to prevent indefinite loading:
// src/contexts/AuthContext.tsx:45-49
const timeoutId = setTimeout(() => {
    console.warn('[AuthContext] fetchProfile timed out, forcing loading to false');
    setLoading(false);
}, 5000);

Code Splitting

All route components are imported directly (not lazy-loaded in current implementation). Consider implementing React.lazy() for larger deployments.

Deployment

Build Process

npm run build
Produces optimized static assets via Vite.

Environment Setup

You need to configure:
  1. Supabase project with migrations applied
  2. Environment variables in hosting platform
  3. DNS configuration for multi-tenant subdomains
  4. OAuth providers (Google) in Supabase dashboard
Ensure all Supabase migrations are applied before deploying. Missing migrations can cause RLS policy failures.

Build docs developers (and LLMs) love