Skip to main content

Overview

SuperTokens Core provides built-in active user tracking that automatically updates user activity timestamps and allows you to query active user counts for any time period.

Features

Automatic Tracking

Activity updated during session operations

Flexible Queries

Count users active in any time period

App-Level Storage

Stored per app, not per tenant

How It Works

Automatic Updates

Active user timestamps are automatically updated during:
  • Session Creation: When a user signs in
  • Session Verification: When an access token is verified
  • Session Refresh: When tokens are refreshed
From io/supertokens/ActiveUsers.java:15-22:
public static void updateLastActive(
    AppIdentifier appIdentifier,
    Main main,
    String userId
) {
    Storage storage = StorageLayer.getStorage(
        appIdentifier.getAsPublicTenantIdentifier(), main
    );
    
    StorageUtils.getActiveUsersStorage(storage)
        .updateLastActive(appIdentifier, userId);
}
Updates are fire-and-forget. Errors are silently ignored to prevent blocking session operations.

Counting Active Users

From io/supertokens/ActiveUsers.java:34-38:
public static int countUsersActiveSince(
    Main main,
    AppIdentifier appIdentifier,
    long time  // Unix timestamp in milliseconds
) {
    Storage storage = StorageLayer.getStorage(
        appIdentifier.getAsPublicTenantIdentifier(), main
    );
    
    return StorageUtils.getActiveUsersStorage(storage)
        .countUsersActiveSince(appIdentifier, time);
}

Example: Common Time Periods

long now = System.currentTimeMillis();

// Daily Active Users (DAU)
long oneDayAgo = now - (24 * 60 * 60 * 1000);
int dau = ActiveUsers.countUsersActiveSince(
    main, appIdentifier, oneDayAgo
);
System.out.println("Daily Active Users: " + dau);

// Weekly Active Users (WAU)
long oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000);
int wau = ActiveUsers.countUsersActiveSince(
    main, appIdentifier, oneWeekAgo
);
System.out.println("Weekly Active Users: " + wau);

// Monthly Active Users (MAU)
long oneMonthAgo = now - (30L * 24 * 60 * 60 * 1000);
int mau = ActiveUsers.countUsersActiveSince(
    main, appIdentifier, oneMonthAgo
);
System.out.println("Monthly Active Users: " + mau);

Manual Updates

While updates are automatic during session operations, you can manually update activity:
ActiveUsers.updateLastActive(
    appIdentifier,
    main,
    userId
);

Use Cases for Manual Updates

  • API requests that don’t create/verify sessions
  • Background jobs or scheduled tasks
  • Mobile app foreground/background transitions
  • WebSocket connections
  • Server-to-server authentication

Account Linking Behavior

From io/supertokens/ActiveUsers.java:40-53: When accounts are linked, activity is consolidated:
public static void updateLastActiveAfterLinking(
    Main main,
    AppIdentifier appIdentifier,
    String primaryUserId,
    String recipeUserId
) {
    ActiveUsersSQLStorage activeUsersStorage = 
        StorageUtils.getActiveUsersStorage(
            StorageLayer.getStorage(
                appIdentifier.getAsPublicTenantIdentifier(), main
            )
        );
    
    // Delete activity for recipe user
    activeUsersStorage.startTransaction(con -> {
        activeUsersStorage.deleteUserActive_Transaction(
            con, appIdentifier, recipeUserId
        );
        return null;
    });
    
    // Update activity for primary user
    updateLastActive(appIdentifier, main, primaryUserId);
}
When accounts are linked, the recipe user’s activity record is deleted and the primary user’s activity is updated. This prevents double-counting.

Storage Scope

Active users are tracked at the app level, not per tenant. The count includes all users active in any tenant within the app.
Active user data is stored in the public tenant storage:
Storage storage = StorageLayer.getStorage(
    appIdentifier.getAsPublicTenantIdentifier(), // Always public tenant
    main
);

Building Analytics Dashboard

Complete Analytics Example

public class UserAnalytics {
    private final Main main;
    private final AppIdentifier appIdentifier;
    
    public UserAnalytics(Main main, AppIdentifier appIdentifier) {
        this.main = main;
        this.appIdentifier = appIdentifier;
    }
    
    public Map<String, Integer> getActiveUserMetrics() 
            throws StorageQueryException, TenantOrAppNotFoundException {
        long now = System.currentTimeMillis();
        Map<String, Integer> metrics = new HashMap<>();
        
        // Hour
        long oneHourAgo = now - (60 * 60 * 1000);
        metrics.put("activeLastHour", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, oneHourAgo)
        );
        
        // Day
        long oneDayAgo = now - (24 * 60 * 60 * 1000);
        metrics.put("activeLastDay", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, oneDayAgo)
        );
        
        // Week
        long oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000);
        metrics.put("activeLastWeek", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, oneWeekAgo)
        );
        
        // Month
        long oneMonthAgo = now - (30L * 24 * 60 * 60 * 1000);
        metrics.put("activeLastMonth", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, oneMonthAgo)
        );
        
        // 3 Months
        long threeMonthsAgo = now - (90L * 24 * 60 * 60 * 1000);
        metrics.put("activeLast3Months", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, threeMonthsAgo)
        );
        
        // 6 Months
        long sixMonthsAgo = now - (180L * 24 * 60 * 60 * 1000);
        metrics.put("activeLast6Months", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, sixMonthsAgo)
        );
        
        // Year
        long oneYearAgo = now - (365L * 24 * 60 * 60 * 1000);
        metrics.put("activeLastYear", 
            ActiveUsers.countUsersActiveSince(main, appIdentifier, oneYearAgo)
        );
        
        return metrics;
    }
    
    public Map<String, Object> getEngagementMetrics() 
            throws StorageQueryException, TenantOrAppNotFoundException {
        long now = System.currentTimeMillis();
        Map<String, Object> engagement = new HashMap<>();
        
        int dau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (24 * 60 * 60 * 1000)
        );
        
        int wau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (7 * 24 * 60 * 60 * 1000)
        );
        
        int mau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (30L * 24 * 60 * 60 * 1000)
        );
        
        engagement.put("dau", dau);
        engagement.put("wau", wau);
        engagement.put("mau", mau);
        
        // Calculate engagement ratios
        if (wau > 0) {
            engagement.put("dauWauRatio", (double) dau / wau);
        }
        
        if (mau > 0) {
            engagement.put("dauMauRatio", (double) dau / mau);
            engagement.put("wauMauRatio", (double) wau / mau);
        }
        
        // Stickiness = DAU / MAU (how often users return)
        if (mau > 0) {
            double stickiness = ((double) dau / mau) * 100;
            engagement.put("stickinessPercent", 
                Math.round(stickiness * 100.0) / 100.0
            );
        }
        
        return engagement;
    }
}

// Usage
UserAnalytics analytics = new UserAnalytics(main, appIdentifier);

Map<String, Integer> metrics = analytics.getActiveUserMetrics();
System.out.println("Active Users Metrics:");
for (Map.Entry<String, Integer> entry : metrics.entrySet()) {
    System.out.println("  " + entry.getKey() + ": " + entry.getValue());
}

Map<String, Object> engagement = analytics.getEngagementMetrics();
System.out.println("\nEngagement Metrics:");
for (Map.Entry<String, Object> entry : engagement.entrySet()) {
    System.out.println("  " + entry.getKey() + ": " + entry.getValue());
}

Example Output

Active Users Metrics:
  activeLastHour: 45
  activeLastDay: 523
  activeLastWeek: 1834
  activeLastMonth: 5621
  activeLast3Months: 12456
  activeLast6Months: 18923
  activeLastYear: 25678

Engagement Metrics:
  dau: 523
  wau: 1834
  mau: 5621
  dauWauRatio: 0.285
  dauMauRatio: 0.093
  wauMauRatio: 0.326
  stickinessPercent: 9.3

Time-Based Cohort Analysis

public class CohortAnalysis {
    public Map<String, Integer> analyzeRetention(
            Main main, 
            AppIdentifier appIdentifier,
            int days
    ) throws StorageQueryException, TenantOrAppNotFoundException {
        long now = System.currentTimeMillis();
        long oneDayMs = 24 * 60 * 60 * 1000;
        Map<String, Integer> retention = new LinkedHashMap<>();
        
        for (int i = 0; i < days; i++) {
            long daysAgo = now - (i * oneDayMs);
            int activeCount = ActiveUsers.countUsersActiveSince(
                main, appIdentifier, daysAgo
            );
            retention.put("day" + i, activeCount);
        }
        
        return retention;
    }
}

// Usage: Analyze 30-day retention
CohortAnalysis cohort = new CohortAnalysis();
Map<String, Integer> retention = cohort.analyzeRetention(
    main, appIdentifier, 30
);

System.out.println("30-Day Retention:");
for (Map.Entry<String, Integer> entry : retention.entrySet()) {
    System.out.println("  " + entry.getKey() + ": " + entry.getValue());
}

Performance Considerations

Database Indexing

Active users table should have an index on:
  • app_id + last_active_time for fast range queries
  • user_id for efficient updates

Caching Strategy

For high-traffic applications, cache active user counts:
public class CachedActiveUsers {
    private final Cache<String, Integer> cache;
    private final Main main;
    private final AppIdentifier appIdentifier;
    
    public CachedActiveUsers(
            Main main, 
            AppIdentifier appIdentifier,
            int cacheTTLMinutes
    ) {
        this.main = main;
        this.appIdentifier = appIdentifier;
        this.cache = CacheBuilder.newBuilder()
            .expireAfterWrite(cacheTTLMinutes, TimeUnit.MINUTES)
            .build();
    }
    
    public int getDAU() throws Exception {
        return cache.get("dau", () -> {
            long oneDayAgo = System.currentTimeMillis() - 
                (24 * 60 * 60 * 1000);
            return ActiveUsers.countUsersActiveSince(
                main, appIdentifier, oneDayAgo
            );
        });
    }
    
    public int getMAU() throws Exception {
        return cache.get("mau", () -> {
            long oneMonthAgo = System.currentTimeMillis() - 
                (30L * 24 * 60 * 60 * 1000);
            return ActiveUsers.countUsersActiveSince(
                main, appIdentifier, oneMonthAgo
            );
        });
    }
    
    public void invalidateCache() {
        cache.invalidateAll();
    }
}

// Usage with 5-minute cache
CachedActiveUsers cachedUsers = new CachedActiveUsers(
    main, appIdentifier, 5
);

int dau = cachedUsers.getDAU();
int mau = cachedUsers.getMAU();

Common Metrics

Key Performance Indicators

DAU/MAU Ratio

Stickiness: Measures how often users return. Higher is better.Formula: (DAU / MAU) × 100%Good: >20%

DAU/WAU Ratio

Weekly Engagement: How active users are within a week.Formula: (DAU / WAU) × 100%Good: >40%

WAU/MAU Ratio

Monthly Engagement: Weekly activity relative to monthly users.Formula: (WAU / MAU) × 100%Good: >50%

User Growth

Growth Rate: Change in active users over time.Formula: ((Current MAU - Previous MAU) / Previous MAU) × 100%

Best Practices

1

Don't Query Too Frequently

Cache results for at least 5-15 minutes to reduce database load
2

Use Appropriate Time Windows

Match time windows to your product’s usage patterns (daily app vs. weekly app)
3

Track Trends Over Time

Store historical counts to analyze growth trends
4

Segment by Cohort

Combine with user metadata to analyze different user segments
5

Monitor Database Performance

Ensure active users table has proper indexes

Limitations

Active user tracking has some limitations:
  • App-level only: Cannot query per-tenant active users directly
  • No deduplication across time: A user active on Day 1 and Day 7 counts twice in a 7-day count
  • Update failures silenced: Errors are ignored to prevent blocking session operations

Historical Data Storage

For long-term analytics, periodically snapshot counts:
public class ActiveUserSnapshot {
    public void snapshotDailyMetrics(
            Main main,
            AppIdentifier appIdentifier
    ) throws Exception {
        long now = System.currentTimeMillis();
        
        // Calculate metrics
        int dau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (24 * 60 * 60 * 1000)
        );
        
        int wau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (7 * 24 * 60 * 60 * 1000)
        );
        
        int mau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (30L * 24 * 60 * 60 * 1000)
        );
        
        // Store in your analytics database
        AnalyticsDB.insertSnapshot(
            appIdentifier.getAppId(),
            now,
            dau,
            wau,
            mau
        );
    }
}

// Run daily via cron job
ActiveUserSnapshot snapshot = new ActiveUserSnapshot();
snapshot.snapshotDailyMetrics(main, appIdentifier);

Integration with Monitoring

// Expose metrics to Prometheus/Grafana
public class ActiveUsersMetricsExporter {
    public void exportMetrics(
            Main main,
            AppIdentifier appIdentifier
    ) throws Exception {
        long now = System.currentTimeMillis();
        
        int dau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (24 * 60 * 60 * 1000)
        );
        
        int mau = ActiveUsers.countUsersActiveSince(
            main, appIdentifier, now - (30L * 24 * 60 * 60 * 1000)
        );
        
        // Export to monitoring system
        MetricsRegistry.gauge("active_users_daily", dau);
        MetricsRegistry.gauge("active_users_monthly", mau);
        MetricsRegistry.gauge("stickiness_percent", 
            mau > 0 ? (double) dau / mau * 100 : 0
        );
    }
}

Build docs developers (and LLMs) love