GetKeyManager is coming soon! 🎉 See here to know what’s new
React Applications and License Enforcement Boundaries

React Applications and License Enforcement Boundaries

Introduction

React developers often ask: “How do I validate licenses in React?” The answer is: you don’t. React runs in the browser, which means any validation code is visible, modifiable, and bypassable by users.

This guide teaches you the correct mental model for license validation in React applications: validation happens on the backend, and React components simply reflect the validated state. You’ll learn where the security boundary lies, how to structure API calls, how to handle entitlement state in React, and how to avoid dangerous anti-patterns.

Whether you’re building a SaaS dashboard, admin panel, analytics tool, or premium content platform, this guide will help you implement entitlement-aware React UIs that work correctly with server-side enforcement.

The Fundamental Rule

License validation MUST happen on the server. React MUST NOT contain validation logic.

Why? Because:

  • JavaScript in the browser is fully visible to users
  • Users can modify any client-side code with DevTools
  • Users can bypass conditional rendering
  • Users can directly call your APIs

React’s job is to:

  1. Call authenticated backend APIs
  2. Display UI based on server responses
  3. Show appropriate messages when capabilities aren’t available

The backend’s job is to:

  1. Validate entitlement keys
  2. Enforce capability restrictions
  3. Protect API endpoints
  4. Return only what the user is authorized to see

Common Mistakes Developers Make

Mistake 1: Client-Side Validation

// ❌ EXTREMELY BAD
function PremiumFeature() {
  const [authorized, setAuthorized] = useState(false);
  
  useEffect(() => {
    // Checking entitlement client-side
    const key = localStorage.getItem('entitlement_key');
    if (key === 'VALID-KEY') {
      setAuthorized(true);
    }
  }, []);
  
  if (!authorized) return null;
  
  return <div>Premium Feature</div>;
}

Why this fails: Users open DevTools, run localStorage.setItem('entitlement_key', 'VALID-KEY'), and bypass the check. Or they simply modify the component code.

Mistake 2: Hardcoding API Keys in React

// ❌ TERRIBLE
const API_KEY = 'your-api-key-here';

function checkLicense() {
  return fetch('https://api.example.com/verify', {
    headers: { 'Authorization': `Bearer ${API_KEY}` }
  });
}

Why this fails: API keys in client code are visible to everyone. Anyone can extract and reuse them.

Mistake 3: Trusting LocalStorage/SessionStorage

// ❌ BAD
const hasAccess = localStorage.getItem('has_premium') === 'true';

if (hasAccess) {
  return <PremiumDashboard />;
}

Why this fails: LocalStorage is fully controllable by users. They can set any value they want.

Mistake 4: Only Hiding UI, Not Protecting APIs

// ❌ INSUFFICIENT
{canExport && <button onClick={() => callExportAPI()}>Export</button>}

// API not protected - users can call it directly via curl
function callExportAPI() {
  fetch('/api/export').then(/*...*/);
}

Why this fails: Hiding buttons doesn’t prevent users from calling APIs directly. APIs must validate independently.

Mistake 5: Passing Entitlement Keys in Props

// ❌ BAD
<PremiumComponent entitlementKey="XXXX-XXXX-XXXX" />

function PremiumComponent({ entitlementKey }) {
  // Using key client-side for decisions
}

Why this fails: Keys in props are visible in React DevTools. Don’t pass keys to the frontend.

The Correct Architecture

Backend: Validation & Enforcement

// Backend API (Node/Express example)
app.post('/api/export', async (req, res) => {
  // Get entitlement key from secure HTTP-only cookie or Authorization header
  const entitlementKey = req.cookies.entitlement_key;
  
  if (!entitlementKey) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // Validate server-side
  const state = await entitlementService.resolveAuthority(
    entitlementKey,
    req.headers.host
  );
  
  // Check capability
  if (!state.features.includes('export')) {
    return res.status(403).json({
      error: 'Export capability not available',
      status: state.status
    });
  }
  
  // Generate export
  const data = await generateExport();
  res.json({ success: true, data });
});

Key points:

  • Entitlement keys come from secure cookies or auth tokens
  • Validation happens server-side, every request
  • APIs return 401/403 when unauthorized
  • Frontend never sees the actual entitlement key

Frontend: Reflect State, Don’t Validate

// React component
import { useState, useEffect } from 'react';

function ExportButton() {
  const [capabilities, setCapabilities] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Fetch user's capabilities from backend
    fetch('/api/user/capabilities', {
      credentials: 'include' // Include cookies
    })
      .then(res => res.json())
      .then(data => {
        setCapabilities(data.capabilities);
        setLoading(false);
      })
      .catch(err => {
        console.error('Failed to load capabilities:', err);
        setLoading(false);
      });
  }, []);
  
  const handleExport = async () => {
    try {
      const response = await fetch('/api/export', {
        method: 'POST',
        credentials: 'include'
      });
      
      if (response.status === 403) {
        const error = await response.json();
        alert(`Export not available: ${error.error}`);
        return;
      }
      
      const result = await response.json();
      // Handle successful export
    } catch (error) {
      alert('Export failed: ' + error.message);
    }
  };
  
  if (loading) return <div>Loading...</div>;
  
  // Show button only if capability exists
  if (!capabilities?.includes('export')) {
    return <div>Export feature not available in your plan.</div>;
  }
  
  return <button onClick={handleExport}>Export Data</button>;
}

Key points:

  • React queries backend for capabilities (never validates keys)
  • HTTP-only cookies carry entitlement keys (inaccessible to JavaScript)
  • UI reflects server-determined state
  • API calls handle 403 errors gracefully
  • No sensitive data in React state

Proper React Patterns

1. Use Context for Entitlement State

// EntitlementContext.js
import { createContext, useContext, useState, useEffect } from 'react';

const EntitlementContext = createContext(null);

export function EntitlementProvider({ children }) {
  const [state, setState] = useState({
    loading: true,
    status: null,
    capabilities: [],
    expiresAt: null
  });
  
  useEffect(() => {
    // Fetch entitlement state from backend
    fetch('/api/entitlement/state', {
      credentials: 'include'
    })
      .then(res => res.json())
      .then(data => {
        setState({
          loading: false,
          status: data.status,
          capabilities: data.capabilities || [],
          expiresAt: data.expiresAt
        });
      })
      .catch(err => {
        console.error('Failed to load entitlement state:', err);
        setState({
          loading: false,
          status: 'ERROR',
          capabilities: [],
          expiresAt: null
        });
      });
  }, []);
  
  return (
    <EntitlementContext.Provider value={state}>
      {children}
    </EntitlementContext.Provider>
  );
}

export function useEntitlement() {
  const context = useContext(EntitlementContext);
  if (context === null) {
    throw new Error('useEntitlement must be used within EntitlementProvider');
  }
  return context;
}

// Helper hooks
export function useHasCapability(capability) {
  const { capabilities } = useEntitlement();
  return capabilities.includes(capability);
}

2. Use Custom Hooks for API Calls

// hooks/useProtectedAPI.js
import { useState } from 'react';

export function useProtectedAPI() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const call = async (endpoint, options = {}) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(endpoint, {
        ...options,
        credentials: 'include' // Always include cookies
      });
      
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.error || `Request failed: ${response.status}`);
      }
      
      const data = await response.json();
      setLoading(false);
      return data;
      
    } catch (err) {
      setError(err.message);
      setLoading(false);
      throw err;
    }
  };
  
  return { call, loading, error };
}

// Usage
function ExportButton() {
  const { call, loading, error } = useProtectedAPI();
  const canExport = useHasCapability('export');
  
  const handleExport = async () => {
    try {
      const result = await call('/api/export', { method: 'POST' });
      alert('Export successful!');
    } catch (err) {
      alert(`Export failed: ${err.message}`);
    }
  };
  
  if (!canExport) {
    return <div>Export not available</div>;
  }
  
  return (
    <button onClick={handleExport} disabled={loading}>
      {loading ? 'Exporting...' : 'Export Data'}
    </button>
  );
}

3. Protected Routes with React Router

// ProtectedRoute.js
import { Navigate } from 'react-router-dom';
import { useEntitlement } from './EntitlementContext';

export function ProtectedRoute({ children, requireCapability }) {
  const { loading, status, capabilities } = useEntitlement();
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (status !== 'ACTIVE') {
    return <Navigate to="/activate" />;
  }
  
  if (requireCapability && !capabilities.includes(requireCapability)) {
    return <Navigate to="/upgrade" />;
  }
  
  return children;
}

// In App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <EntitlementProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/activate" element={<Activate />} />
          
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          
          <Route path="/export" element={
            <ProtectedRoute requireCapability="export">
              <Export />
            </ProtectedRoute>
          } />
          
          <Route path="/analytics" element={
            <ProtectedRoute requireCapability="analytics">
              <Analytics />
            </ProtectedRoute>
          } />
        </Routes>
      </BrowserRouter>
    </EntitlementProvider>
  );
}

4. Graceful Error Handling

function PremiumFeature() {
  const canAccess = useHasCapability('premium_feature');
  const { status, expiresAt } = useEntitlement();
  
  if (status === 'EXPIRED') {
    return (
      <div className="alert alert-warning">
        <h3>Entitlement Expired</h3>
        <p>Your entitlement expired on {new Date(expiresAt).toLocaleDateString()}.</p>
        <a href="/renew">Renew Now</a>
      </div>
    );
  }
  
  if (status === 'SUSPENDED') {
    return (
      <div className="alert alert-error">
        <h3>Account Suspended</h3>
        <p>Please contact support.</p>
      </div>
    );
  }
  
  if (!canAccess) {
    return (
      <div className="alert alert-info">
        <h3>Premium Feature</h3>
        <p>This feature is not included in your plan.</p>
        <a href="/upgrade">Upgrade Now</a>
      </div>
    );
  }
  
  return <PremiumFeatureContent />;
}

What React Should Never Do

❌ Never Validate Entitlement Keys

// DON'T DO THIS
if (entitlementKey === 'VALID-KEY') { }

❌ Never Store Keys in State/LocalStorage

// DON'T DO THIS
const [entitlementKey, setEntitlementKey] = useState('');
localStorage.setItem('entitlement_key', key);

❌ Never Make Validation Decisions

// DON'T DO THIS
const isValid = validateKey(entitlementKey);

❌ Never Pass Keys in Props

// DON'T DO THIS
<Component entitlementKey={key} />

❌ Never Hardcode API Keys

// DON'T DO THIS
const API_KEY = 'secret-key';

What React SHOULD Do

✅ Fetch Capability State from Backend

useEffect(() => {
  fetch('/api/user/capabilities').then(/*...*/);
}, []);

✅ Reflect Server-Determined State

{capabilities.includes('export') && <ExportButton />}

✅ Handle API Errors Gracefully

if (response.status === 403) {
  alert('Feature not available in your plan');
}

✅ Use HTTP-Only Cookies for Authentication

// Backend sets HTTP-only cookie
res.cookie('entitlement_key', key, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

// React includes cookies automatically
fetch('/api/endpoint', { credentials: 'include' });

Example: Complete React + Backend Flow

Backend API

// Node/Express
app.get('/api/user/capabilities', authenticateSession, async (req, res) => {
  const entitlementKey = req.cookies.entitlement_key;
  
  const state = await entitlementService.resolveAuthority(
    entitlementKey,
    req.headers.host
  );
  
  res.json({
    status: state.status,
    capabilities: state.features || [],
    expiresAt: state.expiresAt
  });
});

app.post('/api/export', authenticateSession, async (req, res) => {
  const entitlementKey = req.cookies.entitlement_key;
  const state = await entitlementService.resolveAuthority(entitlementKey, req.headers.host);
  
  if (!state.features.includes('export')) {
    return res.status(403).json({ error: 'Export not available' });
  }
  
  const data = await generateExport();
  res.json({ success: true, data });
});

React Frontend

// App.js
import { EntitlementProvider } from './EntitlementContext';

function App() {
  return (
    <EntitlementProvider>
      <Dashboard />
    </EntitlementProvider>
  );
}

// Dashboard.js
import { useEntitlement, useHasCapability } from './EntitlementContext';
import { useProtectedAPI } from './hooks/useProtectedAPI';

function Dashboard() {
  const { status, expiresAt } = useEntitlement();
  const canExport = useHasCapability('export');
  const canAnalyze = useHasCapability('analytics');
  const { call, loading } = useProtectedAPI();
  
  const handleExport = async () => {
    try {
      await call('/api/export', { method: 'POST' });
      alert('Export completed');
    } catch (err) {
      alert('Export failed: ' + err.message);
    }
  };
  
  return (
    <div>
      <h1>Dashboard</h1>
      
      {status === 'ACTIVE' && (
        <>
          {canExport && (
            <button onClick={handleExport} disabled={loading}>
              Export Data
            </button>
          )}
          
          {canAnalyze && (
            <AnalyticsPanel />
          )}
          
          {expiresAt && (
            <p>Plan expires: {new Date(expiresAt).toLocaleDateString()}</p>
          )}
        </>
      )}
      
      {status === 'EXPIRED' && (
        <div className="alert">
          Your plan has expired. <a href="/renew">Renew now</a>
        </div>
      )}
    </div>
  );
}

Conclusion

React applications must follow a strict security boundary. Key principles:

  1. Validation happens on the backend — Always
  2. React reflects state, never validates — UI is not a security boundary
  3. APIs protect themselves — Every endpoint validates independently
  4. Use HTTP-only cookies — Keep keys out of JavaScript
  5. Fetch capabilities from backend — Server tells client what’s available
  6. Handle errors gracefully — Show appropriate messages for different states
  7. Never trust client-side logic — Assume users can modify everything

Remember: If it’s in React, it’s visible and modifiable. Security MUST be server-side.

Last updated on