vTilt
Why vTiltHow It WorksFeaturesFAQDocs
Docs / NestJS
Quick startEvent forwarding
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceGoogle Ads
Client setup
CursorClaude DesktopVS CodeCodex
Realtime
Debug ViewRealtime Dashboard
Integration guides
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
SDK
Browser SDK
InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdown

Documentation

vTilt
Quick startEvent forwarding

MCP server

Realtime

Debug ViewRealtime Dashboard

Integration guides

NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails

SDK

DocsIntegration guidesNestJS

NestJS

Integrate vTilt with NestJS — injectable provider wrapping the Node SDK, request-scoped capture, graceful shutdown.

NestJS uses dependency injection, so the cleanest integration is a global module that exposes a singleton VTiltService. Your controllers and services then inject it like any other provider.

#1. Install

npm install @v-tilt/node
bash

#2. Create a service

// src/vtilt/vtilt.service.ts
import { Injectable, OnModuleDestroy } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { VTiltNode } from '@v-tilt/node'

@Injectable()
export class VTiltService implements OnModuleDestroy {
  private client: VTiltNode

  constructor(private config: ConfigService) {
    this.client = new VTiltNode(this.config.getOrThrow('VTILT_TRACKER_TOKEN'), {
      host: this.config.get('VTILT_HOST'),
    })
  }

  capture(input: Parameters<VTiltNode['capture']>[0]) {
    return this.client.capture(input)
  }

  identify(input: Parameters<VTiltNode['identify']>[0]) {
    return this.client.identify(input)
  }

  alias(input: Parameters<VTiltNode['alias']>[0]) {
    return this.client.alias(input)
  }

  async onModuleDestroy() {
    await this.client.shutdown()
  }
}
typescript

#3. Register a global module

// src/vtilt/vtilt.module.ts
import { Global, Module } from '@nestjs/common'
import { VTiltService } from './vtilt.service'

@Global()
@Module({
  providers: [VTiltService],
  exports: [VTiltService],
})
export class VTiltModule {}
typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { VTiltModule } from './vtilt/vtilt.module'

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), VTiltModule],
})
export class AppModule {}
typescript
# .env
VTILT_TRACKER_TOKEN=YOUR_PROJECT_TOKEN
VTILT_HOST=https://www.vtilt.com
text

#4. Capture from a controller

// src/checkout/checkout.controller.ts
import { Body, Controller, Post } from '@nestjs/common'
import { VTiltService } from '../vtilt/vtilt.service'

@Controller('checkout')
export class CheckoutController {
  constructor(private vt: VTiltService) {}

  @Post()
  complete(@Body() body: { userId: string; amount: number }) {
    this.vt.capture({
      distinctId: body.userId,
      event: 'purchase_completed',
      properties: { amount: body.amount },
    })
    return { ok: true }
  }
}
typescript

#5. Identify users from a guard or interceptor

If you have an AuthGuard that resolves the current user, identify them once per request so any browser-side anonymous activity merges to the right person record.

// src/vtilt/vtilt.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { VTiltService } from './vtilt.service'

@Injectable()
export class VTiltIdentifyInterceptor implements NestInterceptor {
  constructor(private vt: VTiltService) {}

  intercept(ctx: ExecutionContext, next: CallHandler): Observable<unknown> {
    const req = ctx.switchToHttp().getRequest()
    const user = req.user
    const anonymousId = req.cookies?.vt_anon

    if (user) {
      this.vt.identify({
        distinctId: user.id,
        anonymousId,
        properties: { email: user.email },
      })
    }
    return next.handle()
  }
}
typescript

Note

Note: OnModuleDestroy runs shutdown() so the last batch flushes before the process exits. On serverless platforms (AWS Lambda, Cloud Run) call shutdown() after each handler too — see Node SDK / Context & shutdown.

#Next steps

  • Node SDK / Capture, identify & alias — server-side event shapes.
  • Identify & alias (browser) — anonymous → known user merge model.
  • Event forwarding — fan events out to GA4, Meta CAPI, PostHog.
PreviousDocusaurusIntegration guidesNextHonoIntegration guides

On this page

  • 1. Install
  • 2. Create a service
  • 3. Register a global module
  • 4. Capture from a controller
  • 5. Identify users from a guard or interceptor
  • Next steps