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_idmust correspond to a record in theauth.userstable. - 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 KEYconstraints on anauth.userstable 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_devicestable, create a singleprofilestable. When a Free user eventually signs up via Google, perform an identity migration—update theprofilesrecord to link the newauth.uid()to the existingdevice_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
upsertoperations. 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.