A community based topic aggregation platform built on atproto
1package main 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "log" 8 "net/http" 9 "os" 10 "time" 11 12 "github.com/go-chi/chi/v5" 13 chiMiddleware "github.com/go-chi/chi/v5/middleware" 14 _ "github.com/lib/pq" 15 "github.com/pressly/goose/v3" 16 17 "Coves/internal/api/middleware" 18 "Coves/internal/api/routes" 19 "Coves/internal/core/users" 20 postgresRepo "Coves/internal/db/postgres" 21 "Coves/internal/jetstream" 22) 23 24func main() { 25 // Database configuration (AppView database) 26 dbURL := os.Getenv("DATABASE_URL") 27 if dbURL == "" { 28 // Use dev database from .env.dev 29 dbURL = "postgres://dev_user:dev_password@localhost:5433/coves_dev?sslmode=disable" 30 } 31 32 // Default PDS URL for this Coves instance (supports self-hosting) 33 defaultPDS := os.Getenv("PDS_URL") 34 if defaultPDS == "" { 35 defaultPDS = "http://localhost:3001" // Local dev PDS 36 } 37 38 db, err := sql.Open("postgres", dbURL) 39 if err != nil { 40 log.Fatal("Failed to connect to database:", err) 41 } 42 defer db.Close() 43 44 if err := db.Ping(); err != nil { 45 log.Fatal("Failed to ping database:", err) 46 } 47 48 log.Println("Connected to AppView database") 49 50 // Run migrations 51 if err := goose.SetDialect("postgres"); err != nil { 52 log.Fatal("Failed to set goose dialect:", err) 53 } 54 55 if err := goose.Up(db, "internal/db/migrations"); err != nil { 56 log.Fatal("Failed to run migrations:", err) 57 } 58 59 log.Println("Migrations completed successfully") 60 61 r := chi.NewRouter() 62 63 r.Use(chiMiddleware.Logger) 64 r.Use(chiMiddleware.Recoverer) 65 r.Use(chiMiddleware.RequestID) 66 67 // Rate limiting: 100 requests per minute per IP 68 rateLimiter := middleware.NewRateLimiter(100, 1*time.Minute) 69 r.Use(rateLimiter.Middleware) 70 71 // Initialize repositories and services 72 userRepo := postgresRepo.NewUserRepository(db) 73 userService := users.NewUserService(userRepo, defaultPDS) 74 75 // Start Jetstream consumer for read-forward user indexing 76 jetstreamURL := os.Getenv("JETSTREAM_URL") 77 if jetstreamURL == "" { 78 jetstreamURL = "wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.actor.profile" 79 } 80 81 pdsFilter := os.Getenv("JETSTREAM_PDS_FILTER") // Optional: filter to specific PDS 82 83 userConsumer := jetstream.NewUserEventConsumer(userService, jetstreamURL, pdsFilter) 84 ctx := context.Background() 85 go func() { 86 if err := userConsumer.Start(ctx); err != nil { 87 log.Printf("Jetstream consumer stopped: %v", err) 88 } 89 }() 90 91 log.Printf("Started Jetstream consumer: %s", jetstreamURL) 92 93 // Register XRPC routes 94 routes.RegisterUserRoutes(r, userService) 95 96 r.Get("/health", func(w http.ResponseWriter, r *http.Request) { 97 w.WriteHeader(http.StatusOK) 98 w.Write([]byte("OK")) 99 }) 100 101 port := os.Getenv("APPVIEW_PORT") 102 if port == "" { 103 port = "8081" // Match .env.dev default 104 } 105 106 fmt.Printf("Coves AppView starting on port %s\n", port) 107 fmt.Printf("Default PDS: %s\n", defaultPDS) 108 log.Fatal(http.ListenAndServe(":"+port, r)) 109}