forked from
samuel.fm/statusphere-react
the statusphere demo reworked into a vite/react app in a monorepo
1import events from 'node:events'
2import fs from 'node:fs'
3import type http from 'node:http'
4import path from 'node:path'
5import { DAY, SECOND } from '@atproto/common'
6import compression from 'compression'
7import cors from 'cors'
8import express from 'express'
9import { pino } from 'pino'
10
11import API, { health, oauth } from '#/api'
12import { createClient } from '#/auth/client'
13import { AppContext } from '#/context'
14import { createDb, migrateToLatest } from '#/db'
15import * as error from '#/error'
16import { createBidirectionalResolver, createIdResolver } from '#/id-resolver'
17import { createFirehoseIngester, createJetstreamIngester } from '#/ingestors'
18import { createServer } from '#/lexicons'
19import { env } from '#/lib/env'
20
21export class Server {
22 constructor(
23 public app: express.Application,
24 public server: http.Server,
25 public ctx: AppContext,
26 ) {}
27
28 static async create() {
29 const { NODE_ENV, HOST, PORT, DB_PATH } = env
30 const logger = pino({ name: 'server start' })
31
32 // Set up the SQLite database
33 const db = createDb(DB_PATH)
34 await migrateToLatest(db)
35
36 // Create the atproto utilities
37 const oauthClient = await createClient(db)
38 const baseIdResolver = createIdResolver()
39 const ingester = await createJetstreamIngester(db)
40 // Alternative: const ingester = await createFirehoseIngester(db, baseIdResolver)
41 const resolver = createBidirectionalResolver(baseIdResolver)
42 const ctx = {
43 db,
44 ingester,
45 logger,
46 oauthClient,
47 resolver,
48 }
49
50 // Subscribe to events on the firehose
51 ingester.start()
52
53 const app = express()
54 app.use(cors({ maxAge: DAY / SECOND }))
55 app.use(compression())
56 app.use(express.json())
57 app.use(express.urlencoded({ extended: true }))
58
59 // Create our server
60 let server = createServer({
61 validateResponse: env.isDevelopment,
62 payload: {
63 jsonLimit: 100 * 1024, // 100kb
64 textLimit: 100 * 1024, // 100kb
65 // no blobs
66 blobLimit: 0,
67 },
68 })
69
70 server = API(server, ctx)
71
72 app.use(health.createRouter(ctx))
73 app.use(oauth.createRouter(ctx))
74 app.use(server.xrpc.router)
75 app.use(error.createHandler(ctx))
76
77 // Serve static files from the frontend build - prod only
78 if (env.isProduction) {
79 const frontendPath = path.resolve(
80 __dirname,
81 '../../../packages/client/dist',
82 )
83
84 // Check if the frontend build exists
85 if (fs.existsSync(frontendPath)) {
86 logger.info(`Serving frontend static files from: ${frontendPath}`)
87
88 // Serve static files
89 app.use(express.static(frontendPath))
90
91 // For any other requests, send the index.html file
92 app.get('*', (req, res) => {
93 // Only handle non-API paths
94 if (!req.path.startsWith('/xrpc/')) {
95 res.sendFile(path.join(frontendPath, 'index.html'))
96 } else {
97 res.status(404).json({ error: 'API endpoint not found' })
98 }
99 })
100 } else {
101 logger.warn(`Frontend build not found at: ${frontendPath}`)
102 app.use('*', (_req, res) => {
103 res.sendStatus(404)
104 })
105 }
106 } else {
107 app.set('trust proxy', true)
108 }
109
110 // Use the port from env (should be 3001 for the API server)
111 const httpServer = app.listen(env.PORT)
112 await events.once(httpServer, 'listening')
113 logger.info(
114 `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`,
115 )
116
117 return new Server(app, httpServer, ctx)
118 }
119
120 async close() {
121 this.ctx.logger.info('sigint received, shutting down')
122 await this.ctx.ingester.destroy()
123 await new Promise<void>((resolve) => {
124 this.server.close(() => {
125 this.ctx.logger.info('server closed')
126 resolve()
127 })
128 })
129 }
130}
131
132const run = async () => {
133 const server = await Server.create()
134
135 const onCloseSignal = async () => {
136 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
137 await server.close()
138 process.exit(0)
139 }
140
141 process.on('SIGINT', onCloseSignal)
142 process.on('SIGTERM', onCloseSignal)
143}
144
145run()