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 >
}
/>
Route Allowed Roles Description /dashboardAll authenticated Main dashboard /scheduleAll authenticated Class schedule /membersadmin, coach, receptionist Member management /leadsadmin, receptionist Lead tracking /wodsAll authenticated WOD management /analyticsadmin Analytics dashboard /settingsAll authenticated User settings /billingadmin, receptionist Billing management /rolesadmin Role management /audit-logsadmin Audit log viewer /benchmarksAll authenticated Benchmark tracking /competitionsadmin, coach Competition management /movementsadmin, coach Movement library /profileAll authenticated User profile /adminroot only Super 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:
Tenant Resolution (TenantProvider)
Authentication (AuthProvider)
Profile Fetch (after auth)
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
User visits tenant URL (e.g., crossfitbox.boxora.website)
TenantProvider extracts slug and fetches box data
User submits login credentials
AuthProvider.signIn() calls Supabase Auth
On success, fetch user profile with box_id
Reconcile box_id from tenant context if needed
Fetch box settings and apply theme
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 /> ;
}
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
Produces optimized static assets via Vite.
Environment Setup
You need to configure:
Supabase project with migrations applied
Environment variables in hosting platform
DNS configuration for multi-tenant subdomains
OAuth providers (Google) in Supabase dashboard
Ensure all Supabase migrations are applied before deploying. Missing migrations can cause RLS policy failures.