Skip to main content

Overview

The Next.js SaaS Starter includes a built-in activity logging system that tracks important user and team actions. This provides an audit trail for security, debugging, and analytics purposes.

Database Schema

Activity Logs Table

Activity logs are stored in the activityLogs table:
lib/db/schema.ts
export const activityLogs = pgTable('activity_logs', {
  id: serial('id').primaryKey(),
  teamId: integer('team_id')
    .notNull()
    .references(() => teams.id),
  userId: integer('user_id').references(() => users.id),
  action: text('action').notNull(),
  timestamp: timestamp('timestamp').notNull().defaultNow(),
  ipAddress: varchar('ip_address', { length: 45 }),
});
IP addresses are stored with a maximum length of 45 characters to support both IPv4 and IPv6 addresses.

Database Relations

Activity logs are related to both teams and users:
lib/db/schema.ts
export const activityLogsRelations = relations(activityLogs, ({ one }) => ({
  team: one(teams, {
    fields: [activityLogs.teamId],
    references: [teams.id],
  }),
  user: one(users, {
    fields: [activityLogs.userId],
    references: [users.id],
  }),
}));

Activity Types

The system defines several predefined activity types:
lib/db/schema.ts
export enum ActivityType {
  SIGN_UP = 'SIGN_UP',
  SIGN_IN = 'SIGN_IN',
  SIGN_OUT = 'SIGN_OUT',
  UPDATE_PASSWORD = 'UPDATE_PASSWORD',
  DELETE_ACCOUNT = 'DELETE_ACCOUNT',
  UPDATE_ACCOUNT = 'UPDATE_ACCOUNT',
  CREATE_TEAM = 'CREATE_TEAM',
  REMOVE_TEAM_MEMBER = 'REMOVE_TEAM_MEMBER',
  INVITE_TEAM_MEMBER = 'INVITE_TEAM_MEMBER',
  ACCEPT_INVITATION = 'ACCEPT_INVITATION',
}

Activity Type Categories

ActivityType.SIGN_UP
ActivityType.SIGN_IN
ActivityType.SIGN_OUT
You can extend the ActivityType enum to track additional activities specific to your application.

Logging Activities

The logActivity Function

Activities are logged using the logActivity helper function:
app/(login)/actions.ts
async function logActivity(
  teamId: number | null | undefined,
  userId: number,
  type: ActivityType,
  ipAddress?: string
) {
  if (teamId === null || teamId === undefined) {
    return;
  }
  const newActivity: NewActivityLog = {
    teamId,
    userId,
    action: type,
    ipAddress: ipAddress || ''
  };
  await db.insert(activityLogs).values(newActivity);
}
teamId
number | null | undefined
required
The team ID associated with the activity. If null or undefined, the activity is not logged.
userId
number
required
The ID of the user performing the action.
type
ActivityType
required
The type of activity from the ActivityType enum.
ipAddress
string
The IP address of the user. Defaults to an empty string if not provided.

Example Usage

Here’s how activities are logged during sign-in:
app/(login)/actions.ts
export const signIn = validatedAction(signInSchema, async (data, formData) => {
  // ... authentication logic

  await Promise.all([
    setSession(foundUser),
    logActivity(foundTeam?.id, foundUser.id, ActivityType.SIGN_IN)
  ]);

  redirect('/dashboard');
});

Retrieving Activity Logs

Getting Recent Activities

Use getActivityLogs() to retrieve the most recent activities for the current user:
lib/db/queries.ts
export async function getActivityLogs() {
  const user = await getUser();
  if (!user) {
    throw new Error('User not authenticated');
  }

  return await db
    .select({
      id: activityLogs.id,
      action: activityLogs.action,
      timestamp: activityLogs.timestamp,
      ipAddress: activityLogs.ipAddress,
      userName: users.name
    })
    .from(activityLogs)
    .leftJoin(users, eq(activityLogs.userId, users.id))
    .where(eq(activityLogs.userId, user.id))
    .orderBy(desc(activityLogs.timestamp))
    .limit(10);
}
This query returns the 10 most recent activities ordered by timestamp (newest first).

Return Type

The function returns an array of activity records with joined user data:
type ActivityLogResult = {
  id: number;
  action: string;
  timestamp: Date;
  ipAddress: string | null;
  userName: string | null;
}[];

Common Logging Patterns

Authentication Events

1

Sign Up

Log when a new user creates an account:
await logActivity(teamId, createdUser.id, ActivityType.SIGN_UP);
2

Sign In

Log when a user signs in:
await logActivity(foundTeam?.id, foundUser.id, ActivityType.SIGN_IN);
3

Sign Out

Log when a user signs out:
await logActivity(userWithTeam?.teamId, user.id, ActivityType.SIGN_OUT);

Account Management

const userWithTeam = await getUserWithTeam(user.id);

await logActivity(
  userWithTeam?.teamId,
  user.id,
  ActivityType.UPDATE_PASSWORD
);

Team Operations

await logActivity(
  teamId,
  createdUser.id,
  ActivityType.CREATE_TEAM
);

Type Definitions

The schema exports type definitions for activity logs:
lib/db/schema.ts
export type ActivityLog = typeof activityLogs.$inferSelect;
export type NewActivityLog = typeof activityLogs.$inferInsert;

Best Practices

Always ensure the teamId is valid before logging. The logActivity function silently returns if teamId is null or undefined.
Consider implementing IP address capture from request headers for more detailed audit trails:
const ipAddress = request.headers.get('x-forwarded-for') || request.ip;
await logActivity(teamId, userId, ActivityType.SIGN_IN, ipAddress);
Activity logs are team-scoped, meaning each log entry is associated with a specific team. This ensures proper data isolation in multi-tenant scenarios.

Extending Activity Logging

To add custom activity types:
1

Add to enum

Extend the ActivityType enum in lib/db/schema.ts:
export enum ActivityType {
  // ... existing types
  EXPORT_DATA = 'EXPORT_DATA',
  CHANGE_SETTINGS = 'CHANGE_SETTINGS',
}
2

Log the activity

Use the new type in your actions:
await logActivity(
  teamId,
  userId,
  ActivityType.EXPORT_DATA
);
3

Display in UI

Update your activity log display to handle the new types with appropriate icons and descriptions.

Build docs developers (and LLMs) love