@@ -9,6 +9,9 @@ import { getRateLimitConfig } from './config/rateLimit';
99import { createAdminAuthMiddleware } from './middleware/adminAuth' ;
1010import path from 'path' ;
1111
12+ // Create Fastify instance early; we will either
13+ // - export a Vercel handler that forwards requests to it, or
14+ // - call listen() for local/dev environments.
1215const fastify = Fastify ( {
1316 logger : process . env . NODE_ENV === 'development' ? {
1417 level : process . env . LOG_LEVEL || 'info' ,
@@ -56,62 +59,78 @@ const fastify = Fastify({
5659 } ,
5760 } ,
5861} ) ;
62+ let initialized = false ;
63+ let adminAuthOnce : ReturnType < typeof createAdminAuthMiddleware > | null = null ;
5964
60- async function start ( ) {
61- try {
62- // Initialize admin authentication
63- const adminSecret = process . env . ADMIN_SECRET ;
64- if ( ! adminSecret || adminSecret . length !== 64 ) {
65- console . error ( 'ERROR: ADMIN_SECRET must be a 64-character string' ) ;
65+ async function initApp ( ) {
66+ if ( initialized ) return ;
67+ // Initialize admin authentication
68+ const adminSecret = process . env . ADMIN_SECRET ;
69+ if ( ! adminSecret || adminSecret . length !== 64 ) {
70+ const msg = 'ERROR: ADMIN_SECRET must be a 64-character string' ;
71+ console . error ( msg ) ;
72+ // In serverless environments, avoid exiting the process; throw instead
73+ if ( process . env . VERCEL ) {
74+ throw new Error ( msg ) ;
75+ } else {
6676 console . error ( 'Please set ADMIN_SECRET in your environment variables' ) ;
6777 process . exit ( 1 ) ;
6878 }
69- const adminAuth = createAdminAuthMiddleware ( { adminSecret } ) ;
70-
71- // Initialize rate limiting with configuration
72- const rateLimitConfig = getRateLimitConfig ( ) ;
73- rateLimiterService . updateConfig ( rateLimitConfig ) ;
74-
75- // Register CORS
76- await fastify . register ( cors , {
77- origin : true ,
78- credentials : true ,
79- } ) ;
80-
81- // Register rate limiters
82- await rateLimiterService . registerRateLimiters ( fastify ) ;
83-
84- // Register static files for admin UI
85- await fastify . register ( staticFiles , {
86- root : path . join ( __dirname , 'public' ) ,
87- prefix : '/admin/' ,
88- } ) ;
89-
90- // Add authentication middleware to admin routes
91- fastify . addHook ( 'preHandler' , async ( request , reply ) => {
92- // Only apply admin auth to /admin/* routes
93- if ( request . url . startsWith ( '/admin/' ) ) {
94- await adminAuth ( request , reply ) ;
95- }
96- } ) ;
97-
98- // Register API routes
99- await fastify . register ( cleanRoutes , { prefix : '/api' } ) ;
100- await fastify . register ( strategyRoutes , { prefix : '/api' } ) ;
101-
102- // Health check endpoint
103- fastify . get ( '/health' , async ( _request , _reply ) => {
104- return { status : 'ok' , timestamp : new Date ( ) . toISOString ( ) } ;
105- } ) ;
106-
107- // Root redirect to admin UI (protected)
108- fastify . get ( '/' , {
109- preHandler : adminAuth ,
110- } , async ( _request , reply ) => {
111- return reply . redirect ( '/admin/' ) ;
112- } ) ;
113-
114- // Start server
79+ }
80+ adminAuthOnce = createAdminAuthMiddleware ( { adminSecret } ) ;
81+
82+ // Initialize rate limiting with configuration
83+ const rateLimitConfig = getRateLimitConfig ( ) ;
84+ rateLimiterService . updateConfig ( rateLimitConfig ) ;
85+
86+ // Register CORS
87+ await fastify . register ( cors , {
88+ origin : true ,
89+ credentials : true ,
90+ } ) ;
91+
92+ // Register rate limiters
93+ await rateLimiterService . registerRateLimiters ( fastify ) ;
94+
95+ // Register static files for admin UI
96+ await fastify . register ( staticFiles , {
97+ root : path . join ( __dirname , 'public' ) ,
98+ prefix : '/admin/' ,
99+ } ) ;
100+
101+ // Add authentication middleware to admin routes
102+ fastify . addHook ( 'preHandler' , async ( request , reply ) => {
103+ // Only apply admin auth to /admin/* routes
104+ if ( request . url . startsWith ( '/admin/' ) && adminAuthOnce ) {
105+ await adminAuthOnce ( request , reply ) ;
106+ }
107+ } ) ;
108+
109+ // Register API routes
110+ await fastify . register ( cleanRoutes , { prefix : '/api' } ) ;
111+ await fastify . register ( strategyRoutes , { prefix : '/api' } ) ;
112+
113+ // Health check endpoint (non-prefixed)
114+ fastify . get ( '/health' , async ( _request , _reply ) => {
115+ return { status : 'ok' , timestamp : new Date ( ) . toISOString ( ) } ;
116+ } ) ;
117+
118+ // Root redirect to admin UI (protected)
119+ fastify . get ( '/' , {
120+ preHandler : adminAuthOnce ! ,
121+ } , async ( _request , reply ) => {
122+ return reply . redirect ( '/admin/' ) ;
123+ } ) ;
124+
125+ await fastify . ready ( ) ;
126+ initialized = true ;
127+ }
128+
129+ async function start ( ) {
130+ try {
131+ await initApp ( ) ;
132+
133+ // Start server for local/dev environments
115134 const port = parseInt ( process . env . PORT || '3000' , 10 ) ;
116135 const host = process . env . HOST || 'localhost' ;
117136
@@ -139,4 +158,23 @@ process.on('SIGTERM', async () => {
139158 process . exit ( 0 ) ;
140159} ) ;
141160
142- start ( ) ;
161+ // If running on Vercel (@vercel/node), export a handler instead of listening
162+ // This allows the serverless function to dispatch requests to Fastify without binding a port
163+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164+ export default async function handler ( req : any , res : any ) {
165+ try {
166+ await initApp ( ) ;
167+ // Forward the incoming request to Fastify's underlying Node server
168+ fastify . server . emit ( 'request' , req , res ) ;
169+ } catch ( err ) {
170+ // If initialization fails (e.g., missing ADMIN_SECRET), return 500
171+ res . statusCode = 500 ;
172+ res . setHeader ( 'Content-Type' , 'application/json' ) ;
173+ res . end ( JSON . stringify ( { success : false , error : 'Initialization error' } ) ) ;
174+ }
175+ }
176+
177+ // Start a real server only when not in Vercel/serverless environment
178+ if ( ! process . env . VERCEL ) {
179+ start ( ) ;
180+ }
0 commit comments