# Returning User Experience

### The Problem

Every time a user returns to your site, they start from scratch:

* **E-commerce:** Cart is empty, preferences forgotten, recommendations generic
* **SaaS:** Has to log in again, settings reset, onboarding repeated
* **Content sites:** Sees the same content, no reading history, preferences lost
* **Forms:** Re-enters the same information every time

Traditional solutions have critical limitations:

| Method             | Problem                                                        |
| ------------------ | -------------------------------------------------------------- |
| **Cookies**        | Cleared by users, blocked by browsers, don't survive incognito |
| **Local Storage**  | Same issues as cookies, plus easily wiped                      |
| **Login Required** | Creates friction, many users won't sign up                     |
| **IP Address**     | Changes constantly, shared by many users                       |
| **Email/Phone**    | Requires user to provide personal data upfront                 |

{% hint style="warning" %}
**The result:** You treat every visitor like a stranger, even your most loyal customers.
{% endhint %}

***

### The Solution: Device-Based Recognition

Guardian Stack's `visitorId` persists across sessions, browsers, and cleared cookies. You can recognize returning visitors the moment they land — before they log in, without asking for personal information.

**How It Works**

<div data-with-frame="true"><figure><img src="/files/ensexO6a1A7YqEy3S6IK" alt=""><figcaption></figcaption></figure></div>

1. Visitor lands on your site
2. Guardian SDK silently generates a persistent `visitorId`
3. Your backend checks: Have we seen this `visitorId` before?
4. If yes → Personalize their experience immediately
5. If no → Treat as new visitor, start building their profile

**What You Can Do**

* **Pre-fill forms** with previously entered information
* **Restore cart contents** from abandoned sessions
* **Show relevant recommendations** based on browsing history
* **Skip repeated onboarding** for returning users
* **Streamline authentication** — recognize device, reduce MFA
* **Maintain preferences** — language, currency, theme, settings

***

### Implementation Guide

#### Step 1: Frontend — Identify Visitors on Page Load

Install the Guardian JS SDK:

```bash
npm install @guardianstack/guardian-js
```

Initialize Guardian early and identify the visitor:

```typescript
import { loadAgent } from "@guardianstack/guardian-js";

// Initialize once when your app starts
const guardian = await loadAgent({
  siteKey: "YOUR_SITE_KEY",
});

// Get visitor identity on page load
async function identifyVisitor() {
  const response = await guardian.get();
  const requestId = response?.requestId;

  // Send to your backend to check if returning visitor
  const visitorData = await fetch("/api/identify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ guardianRequestId: requestId }),
  });

  return visitorData.json();
}

// Call on app initialization
const visitor = await identifyVisitor();

if (visitor.isReturning) {
  // Apply personalization
  applyPreferences(visitor.preferences);
  restoreCart(visitor.cart);
  showRelevantContent(visitor.interests);
}
```

#### Step 2: Backend — Recognize and Personalize

Install the Guardian Server SDK:

```bash
npm install @guardianstack/guardianjs-server
```

Create an endpoint to identify visitors and return their profile:

```typescript
import {
  createGuardianClient,
  isBot,
} from "@guardianstack/guardianjs-server";

const guardianClient = createGuardianClient({
  secret: process.env.GUARDIAN_SECRET_KEY!,
});

app.post("/api/identify", async (req, res) => {
  const { guardianRequestId } = req.body;

  // 1. Fetch Guardian event
  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // 2. Skip bots — they don't need personalization
  if (isBot(event)) {
    return res.json({ isReturning: false, isBot: true });
  }

  // 3. Check if we've seen this visitor before
  const visitorProfile = await db.visitorProfiles.findUnique({
    where: { visitorId },
    include: {
      preferences: true,
      cartItems: true,
      browsingHistory: true,
    },
  });

  if (!visitorProfile) {
    // New visitor — create profile for future visits
    await db.visitorProfiles.create({
      data: {
        visitorId,
        firstSeenAt: new Date(),
        lastSeenAt: new Date(),
        visitCount: 1,
      },
    });

    return res.json({
      isReturning: false,
      visitorId, // Use for client-side tracking
    });
  }

  // 4. Returning visitor — update and return their data
  await db.visitorProfiles.update({
    where: { visitorId },
    data: {
      lastSeenAt: new Date(),
      visitCount: { increment: 1 },
    },
  });

  return res.json({
    isReturning: true,
    visitCount: visitorProfile.visitCount + 1,
    preferences: visitorProfile.preferences,
    cart: visitorProfile.cartItems,
    interests: deriveInterests(visitorProfile.browsingHistory),
    lastVisit: visitorProfile.lastSeenAt,
  });
});
```

#### Step 3: Save Visitor Activity

Track visitor behavior to personalize future visits:

```typescript
// Save preferences (language, currency, theme, etc.)
app.post("/api/preferences", async (req, res) => {
  const { guardianRequestId, preferences } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  await db.visitorPreferences.upsert({
    where: { visitorId },
    create: {
      visitorId,
      ...preferences,
    },
    update: preferences,
  });

  return res.json({ success: true });
});

// Save cart for abandoned cart recovery
app.post("/api/cart/save", async (req, res) => {
  const { guardianRequestId, items } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Clear existing cart items
  await db.cartItems.deleteMany({ where: { visitorId } });

  // Save current cart
  await db.cartItems.createMany({
    data: items.map((item) => ({
      visitorId,
      productId: item.productId,
      quantity: item.quantity,
      savedAt: new Date(),
    })),
  });

  return res.json({ success: true });
});

// Track browsing for recommendations
app.post("/api/track/view", async (req, res) => {
  const { guardianRequestId, productId, category, duration } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  await db.browsingHistory.create({
    data: {
      visitorId,
      productId,
      category,
      viewDuration: duration,
      viewedAt: new Date(),
    },
  });

  return res.json({ success: true });
});
```

***

### Real-World Examples

#### E-commerce: Abandoned Cart Recovery

**Scenario:** Visitor adds items to cart, leaves, returns 3 days later — cart is waiting for them.

```typescript
// On page load
app.post("/api/identify", async (req, res) => {
  const event = await guardianClient.getEvent(req.body.guardianRequestId);
  const visitorId = event.identification.visitorId;

  const profile = await db.visitorProfiles.findUnique({
    where: { visitorId },
    include: {
      cartItems: {
        include: { product: true },
        where: {
          savedAt: { gte: subDays(new Date(), 30) }, // Cart valid for 30 days
        },
      },
    },
  });

  if (profile?.cartItems.length > 0) {
    return res.json({
      isReturning: true,
      cart: {
        items: profile.cartItems,
        totalItems: profile.cartItems.reduce((sum, i) => sum + i.quantity, 0),
        message: "Welcome back! Your cart is waiting for you.",
      },
    });
  }

  return res.json({ isReturning: !!profile, cart: null });
});
```

**Frontend implementation:**

```typescript
const visitor = await identifyVisitor();

if (visitor.cart?.items.length > 0) {
  // Show notification
  showToast({
    title: "Welcome back!",
    message: `You have ${visitor.cart.totalItems} items in your cart`,
    action: {
      label: "View Cart",
      onClick: () => navigateTo("/cart"),
    },
  });

  // Restore cart state
  cartStore.setItems(visitor.cart.items);
}
```

#### SaaS: Streamlined Authentication

**Scenario:** Recognized device can log in with just email — no password or MFA needed.

```typescript
app.post("/api/login/start", async (req, res) => {
  const { email, guardianRequestId } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Find user
  const user = await db.users.findUnique({ where: { email } });
  if (!user) {
    return res.status(404).json({ error: "User not found" });
  }

  // Check if this device is recognized for this user
  const knownDevice = await db.userDevices.findFirst({
    where: {
      userId: user.id,
      visitorId,
      isVerified: true,
    },
  });

  if (knownDevice && !isBot(event) && !isTampering(event)) {
    // Recognized device — passwordless login
    const token = generateSessionToken(user);

    await db.userDevices.update({
      where: { id: knownDevice.id },
      data: { lastSeenAt: new Date() },
    });

    return res.json({
      success: true,
      token,
      loginMethod: "recognized_device",
      message: "Welcome back!",
    });
  }

  // Unknown device — require verification
  return res.json({
    requiresVerification: true,
    methods: knownDevice ? ["password"] : ["password", "magic_link"],
  });
});
```

#### Content Site: Personalized Feed

**Scenario:** News site shows articles based on visitor's reading history — no login required.

```typescript
app.get("/api/feed", async (req, res) => {
  const { guardianRequestId } = req.query;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Get visitor's reading history
  const history = await db.browsingHistory.findMany({
    where: { visitorId },
    orderBy: { viewedAt: "desc" },
    take: 100,
  });

  if (history.length === 0) {
    // New visitor — show trending content
    const trending = await getTrendingArticles();
    return res.json({ articles: trending, personalized: false });
  }

  // Analyze interests from reading history
  const categoryCounts = history.reduce((acc, item) => {
    acc[item.category] = (acc[item.category] || 0) + 1;
    return acc;
  }, {});

  const topCategories = Object.entries(categoryCounts)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 3)
    .map(([category]) => category);

  // Get personalized articles
  const articles = await db.articles.findMany({
    where: {
      category: { in: topCategories },
      id: { notIn: history.map((h) => h.articleId) }, // Don't show already read
    },
    orderBy: { publishedAt: "desc" },
    take: 20,
  });

  return res.json({
    articles,
    personalized: true,
    interests: topCategories,
  });
});
```

#### Forms: Smart Pre-fill

**Scenario:** Checkout form remembers shipping address from previous purchases — even for guest checkout.

```typescript
app.get("/api/checkout/prefill", async (req, res) => {
  const { guardianRequestId } = req.query;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Get previously used addresses
  const previousOrders = await db.guestOrders.findMany({
    where: { visitorId },
    orderBy: { createdAt: "desc" },
    take: 1,
    select: {
      shippingAddress: true,
      billingAddress: true,
      email: true,
      phone: true,
    },
  });

  if (previousOrders.length === 0) {
    return res.json({ hasPrefill: false });
  }

  const lastOrder = previousOrders[0];

  return res.json({
    hasPrefill: true,
    prefill: {
      email: maskEmail(lastOrder.email), // Show "j***@example.com"
      phone: maskPhone(lastOrder.phone), // Show "***-***-1234"
      shippingAddress: lastOrder.shippingAddress,
      billingAddress: lastOrder.billingAddress,
    },
  });
});
```

**Frontend implementation:**

```typescript
const prefillData = await fetch(`/api/checkout/prefill?guardianRequestId=${requestId}`);

if (prefillData.hasPrefill) {
  showPrefillPrompt({
    message: "Use your saved information?",
    preview: `${prefillData.prefill.shippingAddress.city}, ${prefillData.prefill.shippingAddress.state}`,
    onConfirm: () => {
      fillForm(prefillData.prefill);
    },
    onDecline: () => {
      // User wants to enter new info
    },
  });
}
```

#### Multi-Session Onboarding

**Scenario:** User starts onboarding, leaves, returns later — picks up where they left off.

```typescript
app.post("/api/onboarding/progress", async (req, res) => {
  const { guardianRequestId, step, data } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Save progress
  await db.onboardingProgress.upsert({
    where: { visitorId },
    create: {
      visitorId,
      currentStep: step,
      stepData: data,
      startedAt: new Date(),
      lastUpdatedAt: new Date(),
    },
    update: {
      currentStep: step,
      stepData: data,
      lastUpdatedAt: new Date(),
    },
  });

  return res.json({ success: true });
});

app.get("/api/onboarding/resume", async (req, res) => {
  const { guardianRequestId } = req.query;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  const progress = await db.onboardingProgress.findUnique({
    where: { visitorId },
  });

  if (!progress || progress.currentStep === "completed") {
    return res.json({ hasProgress: false });
  }

  return res.json({
    hasProgress: true,
    currentStep: progress.currentStep,
    savedData: progress.stepData,
    startedAt: progress.startedAt,
  });
});
```

***

### Linking Visitors to Accounts

When an anonymous visitor signs up or logs in, merge their visitor profile with their account:

```typescript
app.post("/api/auth/complete", async (req, res) => {
  const { userId, guardianRequestId } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Link visitor profile to user account
  const visitorProfile = await db.visitorProfiles.findUnique({
    where: { visitorId },
    include: {
      cartItems: true,
      preferences: true,
      browsingHistory: true,
    },
  });

  if (visitorProfile) {
    // Merge cart items
    if (visitorProfile.cartItems.length > 0) {
      await db.userCartItems.createMany({
        data: visitorProfile.cartItems.map((item) => ({
          userId,
          productId: item.productId,
          quantity: item.quantity,
        })),
        skipDuplicates: true,
      });
    }

    // Merge preferences
    if (visitorProfile.preferences) {
      await db.userPreferences.upsert({
        where: { userId },
        create: {
          userId,
          ...visitorProfile.preferences,
        },
        update: visitorProfile.preferences,
      });
    }

    // Link visitor to user for future recognition
    await db.userDevices.create({
      data: {
        userId,
        visitorId,
        isVerified: true,
        firstSeenAt: visitorProfile.firstSeenAt,
        lastSeenAt: new Date(),
      },
    });
  }

  return res.json({ success: true, profileMerged: !!visitorProfile });
});
```

***

### Database Schema Example

```sql
-- Anonymous visitor profiles
CREATE TABLE visitor_profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  visitor_id VARCHAR(255) UNIQUE NOT NULL,
  first_seen_at TIMESTAMP DEFAULT NOW(),
  last_seen_at TIMESTAMP DEFAULT NOW(),
  visit_count INTEGER DEFAULT 1
);

CREATE INDEX idx_visitor_profiles_visitor_id 
  ON visitor_profiles(visitor_id);

-- Visitor preferences (anonymous)
CREATE TABLE visitor_preferences (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  visitor_id VARCHAR(255) UNIQUE REFERENCES visitor_profiles(visitor_id),
  language VARCHAR(10),
  currency VARCHAR(3),
  theme VARCHAR(20),
  notifications_enabled BOOLEAN DEFAULT true,
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Anonymous cart items
CREATE TABLE visitor_cart_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  visitor_id VARCHAR(255) REFERENCES visitor_profiles(visitor_id),
  product_id UUID NOT NULL,
  quantity INTEGER DEFAULT 1,
  saved_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_visitor_cart_visitor 
  ON visitor_cart_items(visitor_id);

-- Browsing history for recommendations
CREATE TABLE visitor_browsing_history (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  visitor_id VARCHAR(255) REFERENCES visitor_profiles(visitor_id),
  product_id UUID,
  article_id UUID,
  category VARCHAR(100),
  view_duration INTEGER, -- seconds
  viewed_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_browsing_history_visitor 
  ON visitor_browsing_history(visitor_id, viewed_at DESC);

-- Onboarding progress
CREATE TABLE onboarding_progress (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  visitor_id VARCHAR(255) UNIQUE,
  current_step VARCHAR(50),
  step_data JSONB,
  started_at TIMESTAMP DEFAULT NOW(),
  last_updated_at TIMESTAMP DEFAULT NOW()
);

-- Link visitors to user accounts
CREATE TABLE user_devices (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  visitor_id VARCHAR(255) NOT NULL,
  is_verified BOOLEAN DEFAULT false,
  device_name VARCHAR(255),
  first_seen_at TIMESTAMP,
  last_seen_at TIMESTAMP DEFAULT NOW(),
  
  UNIQUE(user_id, visitor_id)
);
```

***

### Privacy Considerations

Guardian's `visitorId` is privacy-friendly:

* **No personal data required** — Recognition works without email, phone, or name
* **Device-based, not person-based** — Identifies the device, not the individual
* **User control** — Clearing browser data resets identity (if they want anonymity)
* **GDPR compliant** — No cookies, no cross-site tracking
* **Transparent** — Users can see and manage their saved preferences

**Best practice:** Let users opt out of personalization:

```typescript
app.post("/api/preferences/reset", async (req, res) => {
  const { guardianRequestId } = req.body;

  const event = await guardianClient.getEvent(guardianRequestId);
  const visitorId = event.identification.visitorId;

  // Delete all visitor data
  await db.visitorProfiles.delete({ where: { visitorId } });

  return res.json({
    success: true,
    message: "Your browsing data has been cleared",
  });
});
```

***

### Best Practices

#### Do

* **Recognize early** — Call Guardian on page load, not just at checkout
* **Be helpful, not creepy** — "Your cart is waiting" is good; "We know you looked at X 47 times" is not
* **Offer control** — Let users clear their data or opt out of personalization
* **Merge on signup** — Transfer anonymous preferences to user accounts
* **Handle gracefully** — If recognition fails, fall back to default experience
* **Respect privacy** — Don't expose `visitorId` to users or third parties

#### Don't

* **Don't require login for personalization** — That defeats the purpose
* **Don't be too aggressive** — Subtle personalization beats obvious tracking
* **Don't store sensitive data** — Financial info, health data, etc. should require authentication
* **Don't assume one device = one person** — Shared devices exist

***

### Conclusion

Every returning visitor is an opportunity. Without recognition, you treat loyal customers like strangers — making them log in repeatedly, re-enter information, and start from scratch.

Guardian Stack's persistent `visitorId` lets you:

* **Recognize visitors instantly** — Before they log in, without cookies
* **Personalize from the first click** — Show relevant content, restore preferences
* **Reduce friction** — Pre-fill forms, streamline authentication
* **Recover abandoned carts** — Bring visitors back to where they left off
* **Build loyalty** — Create seamless experiences that feel personal

The key insight: You don't need personal information to create personal experiences. Device recognition gives you continuity without compromising privacy.

***

{% hint style="success" %}
**Get Started:** [Sign up for Guardian Stack](https://dashboard.guardianstack.ai/) and start recognizing your visitors today.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.guardianstack.ai/documentation/protect-your-implementation/returning-user-experience.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
