Tracking Anonymous Users with Shadow Identities in Supabase

Summary

The engineering team identified a data visibility gap where non-authenticated (Free) users were completely invisible to our analytics and backend logic. Because our Supabase users table was strictly tied to Google OAuth identities, we lacked a way to track user behavior, device state, or feature usage for the majority of our user base. This postmortem explores the architectural decision of implementing Anonymous Identity Tracking using device-specific UUIDs versus formal authentication.

Root Cause

The issue stemmed from a binary authentication model: users were either “Authenticated” (Premium/Google) or “Ghosts” (Free/Unauthenticated).

  • Schema Coupling: The application logic assumed that a user_id must correspond to a record in the auth.users table.
  • Identity Silos: There was no unified identity layer that could bridge the gap between an anonymous device and a registered account.
  • Lifecycle Mismanagement: We failed to account for the unauthenticated user journey, leading to a lack of telemetry for the most critical part of our conversion funnel.

Why This Happens in Real Systems

In distributed systems and mobile environments, engineers often fall into the trap of Identity Oversimplification.

  • Auth-Centric Thinking: Developers often design schemas around the “Happy Path” where every user has a verified email or social identity.
  • Database Constraints: Using FOREIGN KEY constraints on an auth.users table inherently excludes anyone who hasn’t completed the OAuth handshake.
  • State Management Complexity: Real-world mobile lifecycles (uninstalls, OS wipes, factory resets) make persistent identity significantly harder than a simple database entry.

Real-World Impact

  • Inaccurate Funnel Analytics: We could not calculate the conversion rate from “App Install” to “Premium Sign-up” because we couldn’t track the “Install” stage.
  • Loss of User Context: If a free user encountered a bug, we had zero trace of their device configuration or app version in our database.
  • Poor UX Personalization: We could not persist “Free Tier” settings (like theme or onboarding progress) across sessions without a formal account.

Example or Code

To solve this, we implement a Shadow Identity Pattern using a dedicated profiles table that accepts both UUID (from Auth) and device_id (from anonymous users).

-- Create a unified profile table that supports both authenticated and anonymous users
CREATE TABLE public.profiles (
  id UUID PRIMARY KEY, -- This can be the Supabase Auth ID OR a generated device UUID
  is_anonymous BOOLEAN DEFAULT true,
  device_id TEXT UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS (Row Level Security)
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Allow users to read/write their own profile based on the UUID they hold
CREATE POLICY "Users can manage their own profile" 
ON public.profiles 
FOR ALL 
USING (id = auth.uid() OR device_id = current_setting('app.current_device_id', true)::text);

How Senior Engineers Fix It

A senior engineer moves away from “User vs. Non-User” and moves toward “Identity Evolution.”

  • Identity Merging Strategy: Instead of a separate user_devices table, create a single profiles table. When a Free user eventually signs up via Google, perform an identity migration—update the profiles record to link the new auth.uid() to the existing device_id.
  • Hardware-Backed Persistence: To mitigate the “reinstall” problem, use platform-specific secure storage (like iOS Keychain or Android Keystore) to persist the UUID. These stores often survive app uninstalls.
  • Idempotent Upserts: Ensure that the client-side logic uses upsert operations. If a device sends a UUID that already exists, the backend should update the timestamp rather than throwing a constraint error.
  • Decoupling Identity from Auth: Treat “Identity” as a business logic layer and “Authentication” as a security layer.

Why Juniors Miss It

  • The “Perfect World” Fallacy: Juniors often assume that a user is only a “user” once they have logged in. They treat unauthenticated users as “noise” rather than “data.”
  • Relational Rigidity: They tend to create a strictly hierarchical schema (User -> Device) rather than a flattened, polymorphic schema that allows for identity transition.
  • Ignoring the Physical Layer: They overlook how mobile operating systems handle data persistence, assuming that a UUID generated in code is as permanent as a social media account.

Leave a Comment