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}