import { NestFactory } from '@nestjs/core'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; import { ValidationPipe, Logger } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import compression from '@fastify/compress'; import helmet from '@fastify/helmet'; import cookie from '@fastify/cookie'; async function bootstrap() { const dbUrl = process.env.DATABASE_URL || 'NOT_SET'; console.log(`[DEBUG] DATABASE_URL loaded: ${dbUrl.replace(/:[^:]*@/, ':***@')}`); // Mask password // Create Fastify adapter with Pino logging const adapter = new FastifyAdapter({ logger: { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined, }, trustProxy: true, // Required for getting real IP behind reverse proxy bodyLimit: 10 * 1024 * 1024, // 10MB }); const app = await NestFactory.create( AppModule, adapter, { rawBody: true }, ); // Register Fastify plugins await app.register(compression, { encodings: ['gzip', 'deflate'] }); await app.register(helmet, { contentSecurityPolicy: process.env.NODE_ENV === 'production', }); await app.register(cookie); // Enable cookie parsing for JWT auth // CORS configuration (Fastify-native) app.enableCors({ origin: (origin, callback) => { // Parse CORS_ORIGINS from env (comma-separated) const envOrigins = process.env.CORS_ORIGINS?.split(',').map(o => o.trim()) || []; const allowedOrigins = [ 'http://localhost:3000', 'http://localhost:8963', ...envOrigins, ].filter(Boolean); if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS'), false); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], maxAge: 86400, // 24 hours preflight cache }); // Global validation pipe app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, transformOptions: { enableImplicitConversion: true }, }), ); // Swagger documentation const config = new DocumentBuilder() .setTitle('GoHorse Backoffice API') .setDescription('SaaS Administration and Subscription Management') .setVersion('1.0') .addTag('Stripe') .addTag('Plans') .addTag('Admin Dashboard') .addBearerAuth() .build(); SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config)); // Health check endpoint const fastifyInstance = app.getHttpAdapter().getInstance(); fastifyInstance.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); // Start server const port = process.env.BACKOFFICE_PORT || 3001; const host = '0.0.0.0'; // Force 0.0.0.0 to allow container binding even if env injects public IP await app.listen(port, host); const logger = new Logger('Bootstrap'); logger.log(`🚀 Backoffice API running on: http://${host}:${port}`); logger.log(`📚 Swagger docs: http://${host}:${port}/api/docs`); logger.log(`❤️ Health check: http://${host}:${port}/health`); } void bootstrap();