diff --git a/README.md b/README.md index 7b178ba..fffca0b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Statusphere React -A monorepo for the Statusphere application, which includes a React client and a Node.js backend. +A status sharing application built with React and the AT Protocol. -This is a React refactoring of the [example application](https://atproto.com/guides/applications) covering: +This is a React implementation of the [example application](https://atproto.com/guides/applications) covering: - Signin via OAuth - Fetch information about users (profiles) @@ -42,14 +42,60 @@ This all-in-one command makes OAuth development seamless. ### Additional Commands ```bash -# Build both packages -pnpm build +# Build commands +pnpm build # Build frontend first, then backend +pnpm build:appview # Build only the backend +pnpm build:client # Build only the frontend + +# Start commands +pnpm start # Start the server (serves API and frontend) +pnpm start:client # Start frontend development server only +pnpm start:dev # Start both backend and frontend separately (development only) + +# Other utilities +pnpm typecheck # Run type checking +pnpm format # Format all code +``` + +## Deployment + +For production deployment: + +1. Build both packages: + ```bash + pnpm build + ``` + + This will: + - Build the frontend (`packages/client`) first + - Then build the backend (`packages/appview`) + +2. Start the server: + ```bash + pnpm start + ``` -# Run typecheck on both packages -pnpm typecheck +The backend server will: +- Serve the API at `/api/*` endpoints +- Serve the frontend static files from the client's build directory +- Handle client-side routing by serving index.html for all non-API routes -# Format all code -pnpm format +This simplifies deployment to a single process that handles both the API and serves the frontend assets. + +## Environment Variables + +Create a `.env` file in the root directory with: + +``` +# Required for AT Protocol authentication +ATP_SERVICE_DID=did:plc:your-service-did +ATP_CLIENT_ID=your-client-id +ATP_CLIENT_SECRET=your-client-secret +ATP_REDIRECT_URI=https://your-domain.com/oauth-callback + +# Optional +PORT=3001 +SESSION_SECRET=your-session-secret ``` ## Requirements diff --git a/package.json b/package.json index b040d02..55603c7 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,16 @@ "dev:client": "pnpm --filter @statusphere/client dev", "dev:oauth": "node scripts/setup-ngrok.js", "lexgen": "pnpm --filter @statusphere/lexicon build", - "build": "pnpm -r build", - "start": "pnpm -r start", + + "build": "pnpm build:client && pnpm build:appview", + "build:appview": "pnpm --filter @statusphere/appview build", + "build:client": "pnpm --filter @statusphere/client build", + + "start": "pnpm --filter @statusphere/appview start", + "start:dev": "pnpm -r start", + "start:appview": "pnpm --filter @statusphere/appview start", + "start:client": "pnpm --filter @statusphere/client start", + "clean": "pnpm -r clean", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "typecheck": "pnpm -r typecheck" diff --git a/packages/appview/src/index.ts b/packages/appview/src/index.ts index 5f0de08..334b536 100644 --- a/packages/appview/src/index.ts +++ b/packages/appview/src/index.ts @@ -1,5 +1,6 @@ import events from 'node:events' import type http from 'node:http' +import path from 'node:path' import type { OAuthClient } from '@atproto/oauth-client-node' import { Firehose } from '@atproto/sync' import cors from 'cors' @@ -17,6 +18,7 @@ import { import { createIngester } from '#/ingester' import { env } from '#/lib/env' import { createRouter } from '#/routes' +import fs from 'node:fs' // Application state passed to the router and elsewhere export type AppContext = { @@ -119,10 +121,35 @@ export class Server { const router = createRouter(ctx) app.use(express.json()) app.use(express.urlencoded({ extended: true })) - app.use(router) - app.use('*', (_req, res) => { - res.sendStatus(404) - }) + + // API routes + app.use('/api', router) + + // Serve static files from the frontend build + const frontendPath = path.resolve(__dirname, '../../../client/dist') + + // Check if the frontend build exists + if (fs.existsSync(frontendPath)) { + logger.info(`Serving frontend static files from: ${frontendPath}`) + + // Serve static files + app.use(express.static(frontendPath)) + + // For any other requests, send the index.html file + app.get('*', (req, res) => { + // Skip API routes + if (req.path.startsWith('/api/')) { + return res.sendStatus(404) + } + + res.sendFile(path.join(frontendPath, 'index.html')) + }) + } else { + logger.warn(`Frontend build not found at: ${frontendPath}`) + app.use('*', (_req, res) => { + res.sendStatus(404) + }) + } // Use the port from env (should be 3001 for the API server) const server = app.listen(env.PORT) diff --git a/packages/client/package.json b/packages/client/package.json index 03db821..748e23f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,6 +8,7 @@ "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", + "start": "vite preview --port 3000", "clean": "rimraf dist", "typecheck": "tsc --noEmit" },