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:
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:
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:
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
Authentication
Account Management
Team Operations
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:
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.
The ID of the user performing the action.
The type of activity from the ActivityType enum.
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:
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:
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
Sign Up
Log when a new user creates an account: await logActivity ( teamId , createdUser . id , ActivityType . SIGN_UP );
Sign In
Log when a user signs in: await logActivity ( foundTeam ?. id , foundUser . id , ActivityType . SIGN_IN );
Sign Out
Log when a user signs out: await logActivity ( userWithTeam ?. teamId , user . id , ActivityType . SIGN_OUT );
Account Management
Update Password
Update Account
Delete Account
const userWithTeam = await getUserWithTeam ( user . id );
await logActivity (
userWithTeam ?. teamId ,
user . id ,
ActivityType . UPDATE_PASSWORD
);
Team Operations
Create Team
Invite Member
Accept Invitation
Remove Member
await logActivity (
teamId ,
createdUser . id ,
ActivityType . CREATE_TEAM
);
Type Definitions
The schema exports type definitions for activity logs:
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:
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' ,
}
Log the activity
Use the new type in your actions: await logActivity (
teamId ,
userId ,
ActivityType . EXPORT_DATA
);
Display in UI
Update your activity log display to handle the new types with appropriate icons and descriptions.