Skip to content

Activity Log

The Activity Log feature records user and system-level actions. Entries are stored in the platform.activity_logs database table and can be viewed in the Log application.

import { activityLogService } from '$lib/server/activity-log/service';
// Minimal call
await activityLogService.log({ actionKey: 'user.login' });
// Full call
await activityLogService.log({
actionKey: 'plugin.installed',
userId: String(locals.user.id),
resourceType: 'plugin',
resourceId: pluginId,
context: { name: 'my-plugin', version: '1.0.0' }
});

The log() method is fire-and-forget: it never throws, never blocks business logic. On error it only writes console.error.

# Enable/disable activity logging (default: true)
ACTIVITY_LOG_ENABLED=true

When false, all activityLogService.log() calls return immediately — nothing is written to the database.

apps/web/src/
├── lib/server/activity-log/
│ ├── service.ts # ActivityLogService + singleton
│ ├── repository.ts # ActivityLogRepository (findMany, count, insert)
│ ├── types.ts # ActivityEntry, ActivityLogInput, ActivityLogFilters
│ └── interpolate.ts # {{key}} → value substitution
└── apps/log/
├── activity-logs.remote.ts # fetchActivityLogs server action
└── components/
├── ActivityLog.svelte # Table view component
└── activityLogColumns.ts # Column definitions

The actionKey is a translation key stored in the platform.translations table under the activity namespace.

{resource}.{action}
actionKeyDescription
user.loginUser logged in
user.logoutUser logged out
user.profile.updatedProfile updated
user.activatedUser activated
user.deactivatedUser deactivated
user.group.addedUser added to group
user.group.removedUser removed from group
user.role.assignedRole assigned
user.role.removedRole removed
role.createdRole created
role.deletedRole deleted
role.permission.addedPermission added to role
role.permission.removedPermission removed from role
plugin.installedPlugin installed
plugin.uninstalledPlugin uninstalled

Translations must be inserted into platform.translations under the activity namespace:

INSERT INTO platform.translations (namespace, key, locale, value) VALUES
('activity', 'user.login', 'hu', 'Bejelentkezés'),
('activity', 'user.login', 'en', 'Login'),
('activity', 'plugin.installed', 'hu', '{{name}} plugin telepítve'),
('activity', 'plugin.installed', 'en', 'Plugin {{name}} installed');

If no translation exists, the actionKey value is shown as a fallback (e.g. user.login).

Seed file: packages/database/src/seeds/sql/platform/translations_activity.sql

The context field is an optional JSON object whose values can be substituted into the translation template using {{key}} syntax.

activityLogService.log({
actionKey: 'plugin.installed',
context: { name: 'chat-plugin', version: '2.1.0' }
});

Translation template: "Plugin {{name}} installed (v{{version}})" Result: "Plugin chat-plugin installed (v2.1.0)"

FieldTypeRequiredDescription
iduuidyesAuto-generated unique identifier
action_keyvarchar(255)yesTranslation key (e.g. user.login)
user_idvarchar(255)noAffected user ID
resource_typevarchar(100)noResource type (e.g. plugin, user)
resource_idvarchar(255)noResource identifier
contextjsonbnoInterpolation parameters
created_attimestamptzyesAuto-generated timestamp
interface ActivityLogInput {
actionKey: string;
userId?: string;
resourceType?: string;
resourceId?: string;
context?: Record<string, unknown>;
}
interface ActivityEntry {
id: string;
actionKey: string;
translatedAction?: string; // Populated on query
userId?: string;
resourceType?: string;
resourceId?: string;
context?: Record<string, unknown>;
createdAt: string; // ISO 8601
}
  1. Import the service:
import { activityLogService } from '$lib/server/activity-log/service';
  1. Call it after a successful operation:
export const myCommand = command(schema, async (input) => {
const result = await doSomething(input);
if (result.success) {
activityLogService.log({
actionKey: 'resource.action',
userId: String(locals.user.id),
resourceType: 'resource',
resourceId: String(result.id),
context: { /* optional data */ }
});
}
return result;
});
import { fetchActivityLogs } from '$apps/log/activity-logs.remote';
const result = await fetchActivityLogs({
page: 1,
pageSize: 20,
userId: '123', // optional filter
actionKey: 'user.login', // optional filter
sortBy: 'createdAt',
sortOrder: 'desc'
});
if (result.success) {
console.log(result.data); // ActivityEntry[]
console.log(result.pagination); // { page, pageSize, totalCount, totalPages }
}

Required permission: log.activity.view

import { activityLogRepository } from '$lib/server/activity-log/repository';
// Fetch entries
const entries = await activityLogRepository.findMany(
{ userId: '123', limit: 20, offset: 0 },
'en' // locale for translations
);
// Count
const count = await activityLogRepository.count({ userId: '123' });
// Insert
await activityLogRepository.insert({
actionKey: 'user.login',
userId: '123'
});
FileAction keyEvent
apps/users/users.remote.tsuser.group.addedUser added to group
apps/users/users.remote.tsuser.group.removedUser removed from group
apps/users/users.remote.tsuser.role.assignedRole assigned
apps/users/users.remote.tsuser.role.removedRole removed
apps/users/users.remote.tsuser.activatedUser activated
apps/users/users.remote.tsuser.deactivatedUser deactivated
apps/settings/profile.remote.tsuser.profile.updatedProfile updated
apps/plugin-manager/plugins.remote.tsplugin.uninstalledPlugin uninstalled
routes/api/plugins/upload/+server.tsplugin.installedPlugin installed
apps/users/roles.remote.tsrole.createdRole created
apps/users/roles.remote.tsrole.deletedRole deleted
apps/users/roles.remote.tsrole.permission.addedPermission added
apps/users/roles.remote.tsrole.permission.removedPermission removed
Terminál
# Run from apps/web directory
bun test src/lib/server/activity-log