the statusphere demo reworked into a vite/react app in a monorepo

serve frontend from backend

Changed files
+96 -14
packages
appview
src
client
+54 -8
README.md
··· 1 1 # Statusphere React 2 2 3 - A monorepo for the Statusphere application, which includes a React client and a Node.js backend. 3 + A status sharing application built with React and the AT Protocol. 4 4 5 - This is a React refactoring of the [example application](https://atproto.com/guides/applications) covering: 5 + This is a React implementation of the [example application](https://atproto.com/guides/applications) covering: 6 6 7 7 - Signin via OAuth 8 8 - Fetch information about users (profiles) ··· 42 42 ### Additional Commands 43 43 44 44 ```bash 45 - # Build both packages 46 - pnpm build 45 + # Build commands 46 + pnpm build # Build frontend first, then backend 47 + pnpm build:appview # Build only the backend 48 + pnpm build:client # Build only the frontend 49 + 50 + # Start commands 51 + pnpm start # Start the server (serves API and frontend) 52 + pnpm start:client # Start frontend development server only 53 + pnpm start:dev # Start both backend and frontend separately (development only) 54 + 55 + # Other utilities 56 + pnpm typecheck # Run type checking 57 + pnpm format # Format all code 58 + ``` 59 + 60 + ## Deployment 47 61 48 - # Run typecheck on both packages 49 - pnpm typecheck 62 + For production deployment: 50 63 51 - # Format all code 52 - pnpm format 64 + 1. Build both packages: 65 + ```bash 66 + pnpm build 67 + ``` 68 + 69 + This will: 70 + - Build the frontend (`packages/client`) first 71 + - Then build the backend (`packages/appview`) 72 + 73 + 2. Start the server: 74 + ```bash 75 + pnpm start 76 + ``` 77 + 78 + The backend server will: 79 + - Serve the API at `/api/*` endpoints 80 + - Serve the frontend static files from the client's build directory 81 + - Handle client-side routing by serving index.html for all non-API routes 82 + 83 + This simplifies deployment to a single process that handles both the API and serves the frontend assets. 84 + 85 + ## Environment Variables 86 + 87 + Create a `.env` file in the root directory with: 88 + 89 + ``` 90 + # Required for AT Protocol authentication 91 + ATP_SERVICE_DID=did:plc:your-service-did 92 + ATP_CLIENT_ID=your-client-id 93 + ATP_CLIENT_SECRET=your-client-secret 94 + ATP_REDIRECT_URI=https://your-domain.com/oauth-callback 95 + 96 + # Optional 97 + PORT=3001 98 + SESSION_SECRET=your-session-secret 53 99 ``` 54 100 55 101 ## Requirements
+10 -2
package.json
··· 11 11 "dev:client": "pnpm --filter @statusphere/client dev", 12 12 "dev:oauth": "node scripts/setup-ngrok.js", 13 13 "lexgen": "pnpm --filter @statusphere/lexicon build", 14 - "build": "pnpm -r build", 15 - "start": "pnpm -r start", 14 + 15 + "build": "pnpm build:client && pnpm build:appview", 16 + "build:appview": "pnpm --filter @statusphere/appview build", 17 + "build:client": "pnpm --filter @statusphere/client build", 18 + 19 + "start": "pnpm --filter @statusphere/appview start", 20 + "start:dev": "pnpm -r start", 21 + "start:appview": "pnpm --filter @statusphere/appview start", 22 + "start:client": "pnpm --filter @statusphere/client start", 23 + 16 24 "clean": "pnpm -r clean", 17 25 "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", 18 26 "typecheck": "pnpm -r typecheck"
+31 -4
packages/appview/src/index.ts
··· 1 1 import events from 'node:events' 2 2 import type http from 'node:http' 3 + import path from 'node:path' 3 4 import type { OAuthClient } from '@atproto/oauth-client-node' 4 5 import { Firehose } from '@atproto/sync' 5 6 import cors from 'cors' ··· 17 18 import { createIngester } from '#/ingester' 18 19 import { env } from '#/lib/env' 19 20 import { createRouter } from '#/routes' 21 + import fs from 'node:fs' 20 22 21 23 // Application state passed to the router and elsewhere 22 24 export type AppContext = { ··· 119 121 const router = createRouter(ctx) 120 122 app.use(express.json()) 121 123 app.use(express.urlencoded({ extended: true })) 122 - app.use(router) 123 - app.use('*', (_req, res) => { 124 - res.sendStatus(404) 125 - }) 124 + 125 + // API routes 126 + app.use('/api', router) 127 + 128 + // Serve static files from the frontend build 129 + const frontendPath = path.resolve(__dirname, '../../../client/dist') 130 + 131 + // Check if the frontend build exists 132 + if (fs.existsSync(frontendPath)) { 133 + logger.info(`Serving frontend static files from: ${frontendPath}`) 134 + 135 + // Serve static files 136 + app.use(express.static(frontendPath)) 137 + 138 + // For any other requests, send the index.html file 139 + app.get('*', (req, res) => { 140 + // Skip API routes 141 + if (req.path.startsWith('/api/')) { 142 + return res.sendStatus(404) 143 + } 144 + 145 + res.sendFile(path.join(frontendPath, 'index.html')) 146 + }) 147 + } else { 148 + logger.warn(`Frontend build not found at: ${frontendPath}`) 149 + app.use('*', (_req, res) => { 150 + res.sendStatus(404) 151 + }) 152 + } 126 153 127 154 // Use the port from env (should be 3001 for the API server) 128 155 const server = app.listen(env.PORT)
+1
packages/client/package.json
··· 8 8 "build": "tsc && vite build", 9 9 "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 10 "preview": "vite preview", 11 + "start": "vite preview --port 3000", 11 12 "clean": "rimraf dist", 12 13 "typecheck": "tsc --noEmit" 13 14 },