npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aacigroup/aaci_shared

v5.3.0

Published

Shared tracking utilities for AACI Group projects with React Context support

Readme

AACI Shared Library

React Context-based tracking and magic-link library for frontend and backend projects.

npm version

| Client: Frontend Side – React | Client: Backend Side | API Server: Backend Side | | --- | --- | --- |


Quick Links


Features

🔐 Magic Links & Authentication

  • V2 Magic Links with built-in 2FA support (light/strict modes)
  • Email & SMS 2FA delivery
  • Token validation with URL pattern matching
  • Admin tokens for privileged access
  • Full TypeScript support with comprehensive types

📧 External Notifications

  • Email & SMS notification sending
  • Template-based messaging
  • HTML content support
  • Tracking and logging

📊 Lead Tracking & Analytics

  • PostHog integration for analytics
  • Google Tag Manager (GTM) support
  • Lead capture with address validation
  • Session data tracking
  • Environment detection (prod/dev)

⚛️ React Integration

  • Context Providers for easy setup
  • Custom hooks for all features
  • Feature flags support
  • Type-safe throughout

Client: Frontend Side – React

Installation

npm install @aacigroup/aaci_shared react

1. Install the package

npm install @aacigroup/aaci_shared react

2. Set up environment variables

# .env file
VITE_LEAD_CAPTURE_API_URL=https://your-api.com/leads
VITE_LEAD_CAPTURE_API_KEY=your-lead-api-key
VITE_PROJECT_NAME=MyProject
VITE_POSTHOG_KEY=phc_your_production_key
VITE_POSTHOG_DEV_KEY=phc_your_development_key
VITE_PRODUCTION_DOMAINS=myproject.com,www.myproject.com

3. Wrap your app with TrackingProvider

// App.tsx
import { TrackingProvider } from '@aacigroup/aaci_shared/react';

function App() {
  const trackingConfig = {
    apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
    apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
    projectName: import.meta.env.VITE_PROJECT_NAME,
    posthogKey: import.meta.env.VITE_POSTHOG_KEY,
    posthogDevKey: import.meta.env.VITE_POSTHOG_DEV_KEY,
    productionDomains: import.meta.env.VITE_PRODUCTION_DOMAINS.split(',').map(d => d.trim())
  };

  return (
    <TrackingProvider config={trackingConfig}>
      <YourApp />
    </TrackingProvider>
  );
}

4. Use tracking in components

// ContactForm.tsx
import { useLeadTracker, usePostHog } from '@aacigroup/aaci_shared/react';

function ContactForm() {
  const tracker = useLeadTracker();
  const analytics = usePostHog();

  const handleSubmit = async (formData) => {
    // Track lead
    await tracker.trackLeadAndAddress({
      lead_type: 'contact',
      email: formData.email,
      first_name: formData.firstName
    });

    // Track analytics
    analytics.trackEvent('contact_form_submitted');
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

React Context Setup

App Setup

// App.tsx
import { TrackingProvider } from '@aacigroup/aaci_shared/react';

function App() {
  // Create config from environment variables
  const trackingConfig = {
    // Lead Capture API settings
    apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
    apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
    projectName: import.meta.env.VITE_PROJECT_NAME,
    
    // PostHog Analytics settings
    posthogKey: import.meta.env.VITE_POSTHOG_KEY,
    posthogDevKey: import.meta.env.VITE_POSTHOG_DEV_KEY, // Optional: for development
    posthogApiHost: import.meta.env.VITE_POSTHOG_API_HOST, // Optional: for self-hosted PostHog
    
    // Environment detection
    productionDomains: import.meta.env.VITE_PRODUCTION_DOMAINS.split(',').map(d => d.trim())
  };

  return (
    <TrackingProvider config={trackingConfig}>
      <YourApp />
    </TrackingProvider>
  );
}

PostHog Dual Environment Setup

The library automatically uses different PostHog instances based on your environment:

  • Production Environment: Uses VITE_POSTHOG_KEY (only on domains listed in VITE_PRODUCTION_DOMAINS)
  • Development/Staging: Uses VITE_POSTHOG_DEV_KEY (on all other domains/localhost)

This allows you to:

  • Keep production analytics clean and separate from development data
  • Track development/staging events in a separate PostHog project
  • Debug analytics issues without affecting production data

Configuration Options:

# Required: Production PostHog key
VITE_POSTHOG_KEY=phc_your_production_key

# Optional: Development PostHog key (if not provided, PostHog won't initialize in dev)
VITE_POSTHOG_DEV_KEY=phc_your_development_key

Internal Traffic Filtering

Add ?internal=1 to any URL to mark traffic as internal and exclude it from analytics:

https://yoursite.com?internal=1       // Marks as internal
https://yoursite.com?internal=true    // Marks as internal  
https://yoursite.com?internal=0       // Marks as external
https://yoursite.com?internal=false   // Marks as external

Setting persists across page visits until browser storage is cleared.

Component Usage

// ContactForm.tsx
import { 
  useLeadTracker, 
  usePostHog, 
  useStoredLead, 
  useStoredAddress 
} from '@aacigroup/aaci_shared/react';

function ContactForm() {
  const tracker = useLeadTracker();
  const analytics = usePostHog();
  const storedLead = useStoredLead();        // TrackLeadParams | null
  const storedAddress = useStoredAddress();  // AddressInput | null

  const handleSubmit = async (formData) => {
    // Track lead submission
    await tracker.trackLeadAndAddress({
      lead_type: 'contact',
      email: formData.email,
      first_name: formData.firstName
    });

    // Track analytics event
    analytics.trackEvent('contact', { has_email: !!email });
  };

  // Check if user has previously stored data
  const hasStoredData = storedLead || storedAddress;

  return (
    <form onSubmit={handleSubmit}>
      {hasStoredData && <p>We found your previous information!</p>}
      {/* form fields */}
    </form>
  );
}

Feature Flags (Optional)

The library includes optional PostHog-powered feature flags that integrate seamlessly with the tracking system.

Setup

Feature flags are optional and must be explicitly added by wrapping your app with FeatureFlagsProvider inside the TrackingProvider:

// App.tsx
import { 
  TrackingProvider, 
  FeatureFlagsProvider 
} from '@aacigroup/aaci_shared/react';

function App() {
  const trackingConfig = {
    // ... your tracking config
  };

  return (
    <TrackingProvider config={trackingConfig}>
      <FeatureFlagsProvider>
        <YourApp />
      </FeatureFlagsProvider>
    </TrackingProvider>
  );
}

Important: FeatureFlagsProvider must be inside TrackingProvider - it uses the same PostHog instance for consistency.

Component Usage

import { useFeatureFlags } from '@aacigroup/aaci_shared/react';

function MyComponent() {
  const { isFeatureFlagEnabled, getFeatureFlag } = useFeatureFlags();

  // Simple boolean flag
  const showNewDashboard = isFeatureFlagEnabled('new-dashboard');

  // Multi-variant flag (string/boolean values)
  const buttonColor = getFeatureFlag('button-color'); // 'red', 'blue', true, false, etc.

  return (
    <div>
      {showNewDashboard && <NewDashboard />}
      <button className={`btn-${buttonColor}`}>
        Click me
      </button>
    </div>
  );
}

PostHog Configuration

To create and manage feature flags in PostHog:

  1. Access PostHog Dashboard

    • Go to PostHog
    • Navigate to the project matching your API key
  2. Create Feature Flag

    • Go to "Feature Flags" in the left sidebar
    • Click "New Feature Flag"
    • Enter a flag key (e.g., new-dashboard, button-color)
  3. Configure Release

    • For simple on/off flags: Set release condition to "100% of users"
    • For A/B testing: Configure percentage splits
    • For gradual rollouts: Start with lower percentages and increase over time
  4. Save and Deploy

    • Click "Save" - flags are immediately available
    • No code deployment needed for flag value changes

Example Flag Types

// Boolean flags (most common)
const showNewFeature = isFeatureFlagEnabled('new-feature');
const enableBetaMode = isFeatureFlagEnabled('beta-mode');

// Multi-variant flags
const theme = getFeatureFlag('ui-theme');        // 'dark', 'light', 'auto'
const buttonStyle = getFeatureFlag('btn-style'); // 'rounded', 'square', 'pill'
const pricing = getFeatureFlag('pricing-tier');  // 'basic', 'premium', 'enterprise'

// Use in conditional rendering
return (
  <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>
    {showNewFeature && <NewFeature />}
    <button className={`btn-${buttonStyle}`}>
      {pricing === 'premium' ? 'Get Premium' : 'Get Basic'}
    </button>
  </div>
);

Error Handling

If FeatureFlagsProvider is used outside TrackingProvider, you'll see helpful error messages:

// ❌ Wrong - will show console.error
<FeatureFlagsProvider>
  <App />
</FeatureFlagsProvider>

// Console error: "FeatureFlagsProvider must be used within TrackingProvider..."

Advanced Usage

Access the PostHog instance directly if needed:

import { usePostHogFeatureFlags, usePostHog } from '@aacigroup/aaci_shared/react';

function AdvancedComponent() {
  const posthog = usePostHogFeatureFlags();
  const analytics = usePostHog();
  
  // Access PostHog methods directly
  const flagValue = posthog.getInstance().getFeatureFlag('complex-flag');
  
  // Check which PostHog environment is being used
  const envInfo = analytics.getEnvironmentInfo();
  console.log(`Using ${envInfo.environment} PostHog instance`, envInfo);
  
  return <div>Advanced usage</div>;
}

Data Types

Question Types

The library includes a portable question types system for managing dynamic forms and questionnaires.

// Import all question types
import * as QuestionTypes from '@aacigroup/aaci_shared/questionTypes';

// Import specific types
import { 
  TemplateSource,
  QuestionTemplateData,
  EntityQuestionsMap,
  TemplateQuestion
} from '@aacigroup/aaci_shared/questionTypes';

Available exports:

  • All type definitions from types.ts
  • Helper functions from helpers.ts
  • Validators from validators.ts
  • Defaults from defaults.ts
  • Writers from writers.ts
  • Session types from session.ts
  • Template types from template.ts (QuestionTemplate, EntityQuestionsMap, TemplateQuestion, etc.)
  • Answer validators from answerValidators/
  • Conditions from conditions.ts
  • Condition helpers from conditionHelpers.ts

Lead Data

interface TrackLeadParams {
  lead_type: string;                    // Required: Type of lead
  first_name?: string;                  // Optional: First name
  last_name?: string;                   // Optional: Last name  
  email?: string;                       // Optional: Email address
  phone?: string;                       // Optional: Phone number
  extra_data?: Record<string, any>;     // Optional: Additional custom data
}

Common lead_type examples (free string):

  • 'policy_review' - Insurance policy review requests
  • 'address_check' - Property address verification
  • 'contact' - General contact form submissions
  • 'signup' - User registration/signup
  • 'consultation' - Service consultation requests
  • 'quote' - Price quote requests

Note: lead_type is a free string - you can use any value that makes sense for your application.

Validation Rules:

  • lead_type is always required
  • Either email OR phone is required when tracking a lead

Address Data

// Base address interface - only address-related fields
interface Address {
  full_address: string;                 // Required: Complete address string
  place_id: string;                     // Required: Google Places API place ID
  street_address: string;               // Required: Street address component
  street_address_2?: string;            // Optional: Street address line 2 (apt, suite, etc.)
  city: string;                         // Required: City
  state: string;                        // Required: State/Province
  zip_code: string;                     // Required: ZIP/Postal code
  county?: string;                      // Optional: County
  country?: string;                     // Optional: Country
}

// Address input for tracking - includes source and extra_data
interface AddressInput extends Address {
  source: string;                       // Required: Lead type that generated this address
  extra_data?: Record<string, any>;     // Optional: Additional address data
}

Usage:

  • Use Address for pure address data (display, validation)
  • Use AddressInput for tracking operations that require source field

Session Data

interface SessionData {
  ip?: string;                          // Optional: User's IP address
  user_agent?: string;                  // Optional: Browser user agent
  browser?: string;                     // Optional: Browser name
  browser_version?: string;             // Optional: Browser version
  os?: string;                          // Optional: Operating system
  device?: string;                      // Optional: Device type
  referrer?: string;                    // Optional: Referring page URL
  utm_source?: string;                  // Optional: UTM source parameter
  utm_medium?: string;                  // Optional: UTM medium parameter
  utm_campaign?: string;                // Optional: UTM campaign parameter
  landing_page?: string;                // Optional: First page visited
  current_page?: string;                // Optional: Current page URL
  session_id?: string;                  // Optional: Session identifier
  timestamp?: string;                   // Optional: Timestamp of action
  distinct_id?: string;                 // Optional: User identifier
  gclid?: string;                       // Optional: Google Ads click ID
  fbclid?: string;                      // Optional: Facebook click ID
  fbc?: string;                         // Optional: Facebook browser cookie
  fbp?: string;                         // Optional: Facebook pixel cookie
  is_internal?: boolean;                // Optional: Internal traffic flag (auto-detected from URL)
}

Common source examples (free string):

  • 'policy_review' - When tracking address from policy review forms
  • 'address_check' - For standalone address verification
  • 'contact' - From general contact forms
  • 'signup' - During user registration
  • 'consultation' - From consultation request forms
  • 'quote' - From quote request forms

Note: source is a free string - you can use any value to identify where the address came from. It may match lead_type by design but doesn't have to.

Validation Rules:

  • full_address is always required
  • place_id is always required
  • street_address is always required
  • city is always required
  • state is always required
  • zip_code is always required
  • source is always required

Usage Examples

import { useLeadTracker, usePostHog } from '@aacigroup/aaci_shared/react';

function MyComponent() {
  const tracker = useLeadTracker();
  const analytics = usePostHog();

  // 1. Track lead only - Basic contact form
  const handleContactForm = async (formData) => {
    await tracker.trackLeadAndAddress({
      lead_type: 'contact',
      email: formData.email,
      first_name: formData.firstName
    });
    
    analytics.trackEvent('contact_form_submitted');
  };

  // 2. Track address only - Address verification
  const handleAddressCheck = async (address) => {
    await tracker.trackLeadAndAddress(undefined, {
      full_address: address,
      place_id: 'ChIJd8BlQ2BZwokRAFUEcm_qrcA', // Google Places API ID
      street_address: '123 Main St',
      city: 'New York',
      state: 'NY',
      zip_code: '10001',
      source: 'address_check'
    });
    
    analytics.trackEvent('address_checked');
  };

  // 3. Track both together - Policy review with address
  const handlePolicyReview = async (leadData, addressData) => {
    await tracker.trackLeadAndAddress({
      lead_type: 'policy_review',
      email: leadData.email,
      phone: leadData.phone,
      extra_data: { policy_number: leadData.policyNumber }
    }, {
      full_address: addressData.fullAddress,
      place_id: addressData.placeId,
      street_address: addressData.streetAddress,
      city: addressData.city,
      zip_code: addressData.zipCode,
      source: 'policy_review'
    });
    
    analytics.trackEvent('policy_review_started', { 
      has_phone: !!leadData.phone 
    });
  };

  // 4. Multi-step forms
  const handleStepOne = async (leadData) => {
    // Step 1: Collect lead info
    await tracker.trackLeadAndAddress({
      lead_type: 'quote',
      email: leadData.email
    });
  };

  const handleStepTwo = async (addressData) => {
    // Step 2: Add address (automatically merges with saved lead)
    await tracker.trackLeadAndAddress(undefined, {
      full_address: addressData.address,
      place_id: addressData.placeId,
      street_address: addressData.street,
      city: addressData.city,
      zip_code: addressData.zipCode,
      source: 'quote'
    });
    
    analytics.trackEvent('quote_completed');
  };

  return <div>...</div>;
}

Data Persistence

Lead and address data are automatically saved to browser localStorage when successfully tracked:

  • Lead data - Saved to aaci_saved_lead after successful API response
  • Address data - Saved to aaci_saved_address after successful API response
  • Smart merging - If you track only address later, it automatically attaches to saved lead
  • Session persistence - Data persists across page reloads within the same browser

Access Stored Data

import { useStoredLead, useStoredAddress } from '@aacigroup/aaci_shared/react';

function MyComponent() {
  const storedLead = useStoredLead();      // Returns TrackLeadParams | null
  const storedAddress = useStoredAddress(); // Returns Address | null
  
  // Use stored data to pre-fill forms or show personalized content
  if (storedLead) {
    console.log('Previous lead:', storedLead.email);
    console.log('Lead type:', storedLead.lead_type);
  }
  
  if (storedAddress) {
    console.log('Previous address:', storedAddress.full_address);
    console.log('Address source:', storedAddress.source);
  }
}

localStorage Keys

  • aaci_saved_lead - Contains the last successfully tracked lead data
  • aaci_saved_address - Contains the last successfully tracked address data
// Example: Multi-step form automatically merges data
// Step 1: Track lead (saves to localStorage)
await tracker.trackLeadAndAddress({ lead_type: 'quote', email: '[email protected]' });

// Step 2: Track address (merges with saved lead automatically)
await tracker.trackLeadAndAddress(undefined, { 
  full_address: '123 Main St, New York, NY 10001', 
  place_id: 'ChIJd8BlQ2BZwokRAFUEcm_qrcA',
  street_address: '123 Main St',
  city: 'New York',
  zip_code: '10001',
  source: 'quote' 
});

Magic Links

The library includes Magic Link functionality for secure, token-based, login-free access to personalized pages. Magic links are created and validated centrally in your backend and tied to a user profile.

Note: Magic Link is a separate hook from TrackingContext. It requires its own configuration.

Overview

Magic links provide:

  • Customer access tokens - For public URLs to personalized content
  • Admin tokens (optional) - For elevated access/override links
  • URL pattern binding - Tokens are only valid within their expected URL structure
  • Automatic user resolution - Links to user profile by email/phone

React Hook Usage

import { useMagicLink, MagicLinkConfig } from '@aacigroup/aaci_shared/react';

function MagicLinkExample() {
  // Magic Link requires its own config (separate from TrackingConfig)
  const magicLinkConfig: MagicLinkConfig = {
    apiUrl: import.meta.env.VITE_LEAD_CAPTURE_API_URL,
    apiKey: import.meta.env.VITE_LEAD_CAPTURE_API_KEY,
    projectName: import.meta.env.VITE_PROJECT_NAME
  };

  const magicLink = useMagicLink(magicLinkConfig);

  // Create a magic link for a customer
  const handleCreateLink = async () => {
    const result = await magicLink.createMagicLink({
      email: '[email protected]',
      url_pattern: 'https://myapp.com/session/{token}',
      admin_url_pattern: 'https://myapp.com/session/{token}/admin/{admin_token}', // optional
      expires_at: '2025-12-31T23:59:59.000Z' // optional
    });

    if (result.status === 'ok') {
      console.log('Customer URL:', result.url);
      console.log('Admin URL:', result.admin_url); // if admin_url_pattern was provided
      console.log('Token ID:', result.magic_link_token_id);
    }
  };


  // Validate a magic link token from URL (extract token from router params)
  // Example: if your route is /session/:sessionId/:token
  import { useParams } from 'react-router-dom';

  function SessionPage() {
    const { token } = useParams(); // token from route: /session/:sessionId/:token

    const handleValidateLink = async () => {
      const currentUrl = window.location.href;
      const result = await magicLink.validateMagicLink({
        token,
        current_url: currentUrl,
        mode: 'customer' // or 'admin' for admin access
      });

      if (result.valid) {
        console.log('Valid! User ID:', result.person_profile_id);
        console.log('Link data:', result.data); // Data stored in magic link data
        // Render personalized content
      } else {
        console.log('Invalid:', result.reason); // 'expired', 'revoked', 'url_mismatch', 'not_found'
      }
    };

    // Call handleValidateLink on mount or button click
    // useEffect(() => { handleValidateLink(); }, []);
    // or <button onClick={handleValidateLink}>Validate</button>
    return null;
  }

  return <div>...</div>;
}

Create Magic Link

interface CreateMagicLinkParams {
  project_name?: string;             // Optional – auto-populated from config
  email?: string;                    // Optional – used to resolve user
  phone?: string;                    // Optional – alternative user lookup

  url_pattern: string;               // Required – must contain "{token}"
  admin_url_pattern?: string;        // Optional – must contain "{admin_token}"

  expires_at?: string;               // Optional ISO timestamp
  extra_data?: Record<string, any>;  // Optional – stored in magic link data
  session_data?: SessionData;        // Optional – auto-populated if not provided
}

Rules:

  • At least one of: email or phone is required
  • url_pattern is required and must contain {token}
  • If admin_url_pattern is provided, it must contain {admin_token} (and may also include {token})
  • session_data is automatically collected from the browser if not explicitly provided

Response:

interface CreateMagicLinkResponse {
  status: 'ok' | 'error';
  project_name?: string;             // Project identifier
  magic_link_token_id?: string;      // ID of the created magic link
  url?: string;                      // Resolved customer URL
  admin_url?: string;                // Resolved admin URL (if admin_url_pattern provided)
  expires_at?: string;               // Expiration timestamp (if set)
  message?: string;
  errors?: Array<{ field: string; message: string }>;
}

Validate Magic Link

interface ValidateMagicLinkParams {
  project_name?: string;             // Optional – auto-populated from config
  token: string;                     // The token from URL
  current_url: string;               // Full URL being accessed
  mode?: 'customer' | 'admin';       // Default: 'customer'
  session_data?: SessionData;        // Optional – auto-populated if not provided
}

Rules:

  • mode = 'customer' → validates against token + url_pattern
  • mode = 'admin' → validates against admin_token + admin_url_pattern
  • Link must: exist, not be expired, not be revoked, match the expected URL pattern
  • session_data is automatically collected from the browser if not explicitly provided

Response:

interface ValidateMagicLinkResponse {
  status: 'ok' | 'error';
  valid: boolean;
  project_name?: string;             // Project identifier
  magic_link_token_id?: string;
  person_profile_id?: string;
  data?: Record<string, any>;        // Data stored in magic link data
  message?: string;
  reason?: 'expired' | 'revoked' | 'url_mismatch' | 'not_found';  // if invalid
}

Full Page Access Example

import { useMagicLink } from '@aacigroup/aaci_shared/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; // or your router of choice

function ProtectedSessionPage() {
  const magicLink = useMagicLink();
  const { token, adminToken } = useParams(); // Get tokens from React Router
  const [accessState, setAccessState] = useState<'loading' | 'valid' | 'invalid'>('loading');
  const [personId, setPersonId] = useState<string | null>(null);
  const [linkData, setLinkData] = useState<Record<string, any> | null>(null);
  const [isAdmin, setIsAdmin] = useState(false);

  useEffect(() => {
    const validateAccess = async () => {
      const currentUrl = window.location.href;
      
      // Try admin token first if available
      if (adminToken) {
        const result = await magicLink.validateMagicLink({
          token: adminToken,
          current_url: currentUrl,
          mode: 'admin'
        });
        
        if (result.valid) {
          setAccessState('valid');
          setPersonId(result.person_profile_id || null);
          setLinkData(result.data || null);
          setIsAdmin(true);
          return;
        }
      }

      // Fall back to customer token
      if (token) {
        const result = await magicLink.validateMagicLink({
          token,
          current_url: currentUrl,
          mode: 'customer'
        });
        
        if (result.valid) {
          setAccessState('valid');
          setPersonId(result.person_profile_id || null);
          setLinkData(result.data || null);
          return;
        }
      }

      setAccessState('invalid');
    };

    validateAccess();
  }, [token, adminToken]);

  if (accessState === 'loading') return <div>Validating access...</div>;
  if (accessState === 'invalid') return <div>Access denied. Invalid or expired link.</div>;

  return (
    <div>
      {isAdmin && <div className="admin-banner">Admin Mode</div>}
      <h1>Welcome to your personalized session</h1>
      {/* Render personalized content using personId and linkData */}
    </div>
  );
}

Security Notes

  • Projects must NOT store tokens - Only store magic_link_token_id in your database entities
  • Tokens are URL-bound - A token is only valid when accessed via its designated URL pattern
  • Tokens are strong - 32-64 character alphanumeric strings
  • Your backend should be the single source of truth for token storage and validation

Non-React and Backend Usage

For Client: Backend Side and API Server: Backend Side usage patterns (including MagicLink usage in edge/API functions and server-only validation), see:

  • CLIENT_BACKEND.md
  • API_SERVER.md

Troubleshooting

PostHog 401 Unauthorized Errors

If you see console errors like:

GET https://your-api.com/functions/v1/capture-lead-with-address/array/... 401 (Unauthorized)
POST https://your-api.com/functions/v1/capture-lead-with-address/flags/... 401 (Unauthorized)
[PostHog.js] Bad HTTP status: 401 {"status":"error","message":"Unauthorized"}

Cause: PostHog is trying to make requests to your lead capture API instead of PostHog's servers.

Solutions:

  1. Don't set posthogApiHost unless you have a self-hosted PostHog instance:

    // ❌ Wrong - this will cause 401 errors
    const trackingConfig = {
      apiUrl: 'https://your-api.com/leads',
      posthogApiHost: 'https://your-api.com/leads', // Don't do this!
      posthogKey: 'phc_...'
    };
    
    // ✅ Correct - let PostHog use its default servers  
    const trackingConfig = {
      apiUrl: 'https://your-api.com/leads',
      // posthogApiHost: not needed for PostHog Cloud
      posthogKey: 'phc_...'
    };
  2. For PostHog Cloud users (most common):

    • Remove or don't set posthogApiHost in your config
    • PostHog will automatically use https://us.i.posthog.com
  3. For self-hosted PostHog users:

    const trackingConfig = {
      apiUrl: 'https://your-api.com/leads',        // Your API
      posthogApiHost: 'https://your-posthog.com',  // Your PostHog instance
      posthogKey: 'phc_...'
    };

Other Common Issues

Environment Variables Not Loading:

  • Make sure your .env file is in the project root
  • Restart your development server after adding new environment variables
  • Verify variable names start with VITE_ for Vite projects

PostHog Not Initializing:

  • Check browser console for initialization messages
  • Verify your PostHog API key format: phc_...
  • Make sure productionDomains matches your actual domain

Lead Tracking API Errors:

  • Verify VITE_LEAD_CAPTURE_API_URL points to your actual API
  • Check that VITE_LEAD_CAPTURE_API_KEY is correctly set
  • Ensure your API supports the expected request format

License

ISC