Files
twenty/packages/twenty-server/src/engine/core-modules/audit
Charles BochetandGitHub 9e21e55db4 Prevent leak between /metadata and /graphql GQL schemas (#17845)
## Fix resolver schema leaking between `/metadata` and `/graphql`
endpoints

### Summary
- Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that
filters resolvers at both schema generation and runtime, preventing
cross-endpoint leaking
- Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to
explicitly scope each resolver to its endpoint
- Move most resolvers (auth, billing, workspace, user, etc.) to the
metadata schema where the frontend expects them; only workflow and
timeline calendar/messaging resolvers remain on `/graphql`
- Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata)
Apollo client instead of the core client

### Problem
NestJS GraphQL's module-based resolver discovery traverses transitive
imports, causing resolvers from `/metadata` modules to leak into the
`/graphql` schema and vice versa. This made the schemas unpredictable
and tightly coupled to module import order.

### Approach
- Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on
`@nestjs/graphql`, filtering in both `filterResolvers()` (runtime
binding) and `getAllCtors()` (schema generation)
- Each resolver is explicitly decorated with `@CoreResolver()` or
`@MetadataResolver()`
- Organized decorator, constant, and type files under `graphql-config/`
following project conventions


Core GQL Schema: (see: no more fields!)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6"
/>

Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71"
/>
2026-02-11 10:05:24 +00:00
..
2026-02-04 13:30:55 +01:00
2026-02-04 13:30:55 +01:00
2026-02-04 13:30:55 +01:00

Analytics Module

This module provides analytics tracking functionality for the Twenty application.

Usage

Tracking Events

The AuditService provides a createContext method that returns an object with three methods:

  • insertWorkspaceEvent: For tracking workspace-level events
  • createObjectEvent: For tracking object-level events that include record and metadata IDs
  • createPageviewEvent: For tracking page views
import { Injectable } from '@nestjs/common';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';

@Injectable()
export class MyService {
  constructor(private readonly auditService: AuditService) {}

  async doSomething() {
    // Create an analytics context
    const auditService = this.auditService.createContext({
      workspaceId: 'workspace-id',
      userId: 'user-id',
    });

    // Track a workspace event
    auditService.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
    
    // Track an object event
    auditService.createObjectEvent(OBJECT_RECORD_CREATED_EVENT, {
      recordId: 'record-id',
      objectMetadataId: 'object-metadata-id',
      // other properties
    });
    
    // Track a pageview
    auditService.createPageviewEvent('page-name', {
      href: '/path',
      locale: 'en-US',
      // other properties
    });
  }
}

Adding New Events

To add a new event:

  1. Create a new file in the src/engine/core-modules/analytics/utils/events/track directory
  2. Define the event name, schema, and type
  3. Register the event using the registerEvent function
  4. Update the TrackEventName and TrackEventProperties types in src/engine/core-modules/analytics/utils/events/event-types.ts

Example:

// src/engine/core-modules/analytics/utils/events/track/my-feature/my-event.ts
import { z } from 'zod';

import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';

export const MY_EVENT = 'My Event' as const;
export const myEventSchema = z.object({
  event: z.literal(MY_EVENT),
  properties: z.object({
    myProperty: z.string(),
  }),
});

export type MyEventTrackEvent = z.infer<typeof myEventSchema>;

registerEvent(MY_EVENT, myEventSchema);

Then update the events.type.ts file:

// src/engine/core-modules/analytics/types/events.type.ts
import { MY_EVENT, MyEventTrackEvent } from '../utils/events/track/my-feature/my-event';

// Add to the union type
export type TrackEventName = 
  | typeof MY_EVENT
  // ... other event names;

// Add to the TrackEvents interface
export interface TrackEvents {
  [MY_EVENT]: MyEventTrackEvent;
  // ... other event types
}

// The TrackEventProperties type will automatically use the new event
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
  ? TrackEvents[T]['properties']
  : object;

API

AuditService

createContext(context?)

Creates an analytics context with the given user ID and workspace ID.

  • context (optional): An object with userId and workspaceId properties

Returns an object with the following methods:

  • insertWorkspaceEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T>): Tracks a workspace-level event
  • createObjectEvent<T extends TrackEventName>(event: T, properties: TrackEventProperties<T> & { recordId: string; objectMetadataId: string }): Tracks an object-level event
  • createPageviewEvent(name: string, properties: Partial<PageviewProperties>): Tracks a pageview

Types

TrackEventName

A union type of all registered event names, plus string for backward compatibility.

TrackEventProperties

A mapped type that maps each event name to its corresponding properties type. It uses the TrackEvents interface to provide a more maintainable and type-safe way to map event names to their properties.

// Define the mapping between event names and their event types
export interface TrackEvents {
  [EVENT_NAME_1]: Event1Type;
  [EVENT_NAME_2]: Event2Type;
  // ... other event types
}

// Use the mapping to extract properties for each event type
export type TrackEventProperties<T extends TrackEventName> = T extends keyof TrackEvents
  ? TrackEvents[T]['properties']
  : object;

This approach makes it easier to add new events without having to modify a complex nested conditional type.

PageviewProperties

Properties for pageview events, including href, locale, pathname, referrer, sessionId, timeZone, and userAgent.