// Command leaf is the Felt tournament engine binary. It starts all embedded // infrastructure (LibSQL, NATS JetStream, WebSocket hub) and serves the // SvelteKit SPA over HTTP. package main import ( "context" "flag" "log" "net/http" "os" "os/signal" "syscall" "time" feltauth "github.com/felt-app/felt/internal/auth" "github.com/felt-app/felt/internal/clock" feltnats "github.com/felt-app/felt/internal/nats" "github.com/felt-app/felt/internal/server" "github.com/felt-app/felt/internal/server/middleware" "github.com/felt-app/felt/internal/server/ws" "github.com/felt-app/felt/internal/store" ) func main() { dataDir := flag.String("data-dir", "./data", "Data directory for database and files") addr := flag.String("addr", ":8080", "HTTP listen address") devMode := flag.Bool("dev", false, "Enable development mode (permissive CORS, dev seed data)") flag.Parse() log.SetFlags(log.LstdFlags | log.Lmsgprefix) log.SetPrefix("felt: ") log.Printf("starting (data-dir=%s, addr=%s, dev=%v)", *dataDir, *addr, *devMode) // Create root context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // ---- 1. LibSQL Database ---- db, err := store.Open(*dataDir, *devMode) if err != nil { log.Fatalf("failed to open database: %v", err) } defer db.Close() // Verify database is working var one int if err := db.QueryRow("SELECT 1").Scan(&one); err != nil { log.Fatalf("database health check failed: %v", err) } log.Printf("database ready") // ---- 2. Embedded NATS Server ---- natsServer, err := feltnats.Start(ctx, *dataDir) if err != nil { log.Fatalf("failed to start NATS: %v", err) } defer natsServer.Shutdown() // ---- 3. JWT Signing Key (persisted in LibSQL) ---- signingKey, err := feltauth.LoadOrCreateSigningKey(db.DB) if err != nil { log.Fatalf("failed to load/create signing key: %v", err) } // ---- 4. Auth Service ---- jwtService := feltauth.NewJWTService(signingKey, 7*24*time.Hour) // 7-day expiry authService := feltauth.NewAuthService(db.DB, jwtService) // ---- 5. WebSocket Hub ---- tokenValidator := func(tokenStr string) (string, string, error) { return middleware.ValidateJWT(tokenStr, signingKey) } // Tournament validator stub -- allows all for now // TODO: Implement tournament existence + access check against DB tournamentValidator := func(tournamentID string, operatorID string) error { return nil // Accept all tournaments for now } var allowedOrigins []string if *devMode { allowedOrigins = nil // InsecureSkipVerify will be used } else { allowedOrigins = []string{"*"} // Same-origin enforced by browser } hub := ws.NewHub(tokenValidator, tournamentValidator, allowedOrigins) defer hub.Shutdown() // ---- 6. Clock Registry ---- clockRegistry := clock.NewRegistry(hub) defer clockRegistry.Shutdown() log.Printf("clock registry ready") // ---- 7. HTTP Server ---- srv := server.New(server.Config{ Addr: *addr, SigningKey: signingKey, DevMode: *devMode, }, db.DB, natsServer.Server(), hub, authService, clockRegistry) // Start HTTP server in goroutine go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("HTTP server error: %v", err) } }() log.Printf("ready (addr=%s, dev=%v)", *addr, *devMode) // ---- Signal Handling ---- sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh log.Printf("received signal: %s, shutting down...", sig) // Graceful shutdown in reverse startup order shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() // 7. HTTP Server if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("HTTP server shutdown error: %v", err) } // 6. Clock Registry (closed by defer) // 5. WebSocket Hub (closed by defer) // 4. NATS Server (closed by defer) // 3. Database (closed by defer) cancel() // Cancel root context log.Printf("shutdown complete") }