···2020# Install dependencies
2121pnpm install
22222323-# Option 1: Local development (login won't work due to OAuth requirements)
2423pnpm dev
2525-2626-# Option 2: Development with OAuth login support (recommended)
2727-pnpm dev:oauth
2824```
2929-3030-### OAuth Development
3131-3232-Due to OAuth requirements, HTTPS is needed for development. We've made this easy:
3333-3434-- `pnpm dev:oauth` - Sets up everything automatically:
3535- 1. Starts ngrok to create an HTTPS tunnel
3636- 2. Configures environment variables with the ngrok URL
3737- 3. Starts both the API server and client app
3838- 4. Handles proper shutdown of all processes
3939-4040-This all-in-one command makes OAuth development seamless.
41254226### Additional Commands
4327···89739074## Environment Variables
91759292-Create a `.env` file in the root directory with:
7676+Copy the `.env.template` file in the appview to `.env`:
93779478```
9595-# Required for AT Protocol authentication
9696-ATP_SERVICE_DID=did:plc:your-service-did
9797-ATP_CLIENT_ID=your-client-id
9898-ATP_CLIENT_SECRET=your-client-secret
9999-ATP_REDIRECT_URI=https://your-domain.com/oauth-callback
100100-101101-# Optional
102102-PORT=3001
103103-SESSION_SECRET=your-session-secret
7979+cd packages/appview
8080+cp .env.template .env
10481```
1058210683## Requirements
1078410885- Node.js 18+
10986- pnpm 9+
110110-- ngrok (for OAuth development)
1118711288## License
11389
···11+# Environment Configuration
22+NODE_ENV="development" # Options: 'development', 'production'
33+PORT="3001" # The port your server will listen on
44+VITE_PORT="3000" # The port the vite dev server is on (dev only)
55+HOST="127.0.0.1" # Hostname for the server
66+PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id.
77+DB_PATH=":memory:" # The SQLite database path. Leave as ":memory:" to use a temporary in-memory database.
88+99+# Secrets
1010+# Must set this in production. May be generated with `openssl rand -base64 33`
1111+# COOKIE_SECRET=""
+9-16
packages/appview/src/auth/client.ts
···55import { SessionStore, StateStore } from './storage'
6677export const createClient = async (db: Database) => {
88- // Get the ngrok URL from environment variables
99- const ngrokUrl = env.NGROK_URL
1010-1111- if (!ngrokUrl && env.NODE_ENV === 'development') {
1212- console.warn(
1313- 'WARNING: NGROK_URL is not set. OAuth login might not work properly.',
1414- )
1515- console.warn(
1616- 'You should run ngrok and set the NGROK_URL environment variable.',
1717- )
1818- console.warn('Example: NGROK_URL=https://abcd-123-45-678-90.ngrok.io')
1919- } else if (env.NODE_ENV === 'production' && !env.PUBLIC_URL) {
88+ if (env.isProduction && !env.PUBLIC_URL) {
209 throw new Error('PUBLIC_URL is not set')
2110 }
22112323- const baseUrl = ngrokUrl || env.PUBLIC_URL || `http://127.0.0.1:${env.PORT}`
1212+ const publicUrl = env.PUBLIC_URL
1313+ const url = publicUrl || `http://127.0.0.1:${env.VITE_PORT}`
1414+ const enc = encodeURIComponent
24152516 return new NodeOAuthClient({
2617 clientMetadata: {
2718 client_name: 'Statusphere React App',
2828- client_id: `${baseUrl}/api/client-metadata.json`,
2929- client_uri: baseUrl,
3030- redirect_uris: [`${baseUrl}/api/oauth/callback`],
1919+ client_id: publicUrl
2020+ ? `${url}/api/client-metadata.json`
2121+ : `http://localhost?redirect_uri=${enc(`${url}/api/oauth/callback`)}&scope=${enc('atproto transition:generic')}`,
2222+ client_uri: url,
2323+ redirect_uris: [`${url}/api/oauth/callback`],
3124 scope: 'atproto transition:generic',
3225 grant_types: ['authorization_code', 'refresh_token'],
3326 response_types: ['code'],
+31-45
packages/appview/src/index.ts
···7979 'http://127.0.0.1:3000', // Alternative React address
8080 ]
81818282- // If we have an ngrok URL defined, add it to allowed origins
8383- if (env.NGROK_URL) {
8484- try {
8585- const ngrokOrigin = new URL(env.NGROK_URL)
8686- const ngrokClientOrigin = `${ngrokOrigin.protocol}//${ngrokOrigin.hostname}:3000`
8787- allowedOrigins.push(ngrokClientOrigin)
8888- } catch (err) {
8989- console.error('Failed to parse NGROK_URL for CORS:', err)
9090- }
9191- }
9292-9382 // Check if the request origin is in our allowed list or is an ngrok domain
9494- if (
9595- allowedOrigins.indexOf(origin) !== -1 ||
9696- origin.includes('ngrok-free.app')
9797- ) {
8383+ if (allowedOrigins.indexOf(origin) !== -1) {
9884 callback(null, true)
9985 } else {
10086 console.warn(`⚠️ CORS blocked origin: ${origin}`)
···122108 app.use(express.json())
123109 app.use(express.urlencoded({ extended: true }))
124110125125- // Two versions of the API routes:
126126- // 1. Mounted at /api for the client
127111 app.use('/api', router)
128112129129- // Serve static files from the frontend build
130130- const frontendPath = path.resolve(
131131- __dirname,
132132- '../../../packages/client/dist',
133133- )
113113+ // Serve static files from the frontend build - prod only
114114+ if (env.isProduction) {
115115+ const frontendPath = path.resolve(
116116+ __dirname,
117117+ '../../../packages/client/dist',
118118+ )
134119135135- // Check if the frontend build exists
136136- if (fs.existsSync(frontendPath)) {
137137- logger.info(`Serving frontend static files from: ${frontendPath}`)
120120+ // Check if the frontend build exists
121121+ if (fs.existsSync(frontendPath)) {
122122+ logger.info(`Serving frontend static files from: ${frontendPath}`)
138123139139- // Serve static files
140140- app.use(express.static(frontendPath))
124124+ // Serve static files
125125+ app.use(express.static(frontendPath))
141126142142- // Heathcheck
143143- app.get('/health', (req, res) => {
144144- res.status(200).json({ status: 'ok' })
145145- })
127127+ // Heathcheck
128128+ app.get('/health', (req, res) => {
129129+ res.status(200).json({ status: 'ok' })
130130+ })
146131147147- // For any other requests, send the index.html file
148148- app.get('*', (req, res) => {
149149- // Only handle non-API paths
150150- if (!req.path.startsWith('/api/')) {
151151- res.sendFile(path.join(frontendPath, 'index.html'))
152152- } else {
153153- res.status(404).json({ error: 'API endpoint not found' })
154154- }
155155- })
156156- } else {
157157- logger.warn(`Frontend build not found at: ${frontendPath}`)
158158- app.use('*', (_req, res) => {
159159- res.sendStatus(404)
160160- })
132132+ // For any other requests, send the index.html file
133133+ app.get('*', (req, res) => {
134134+ // Only handle non-API paths
135135+ if (!req.path.startsWith('/api/')) {
136136+ res.sendFile(path.join(frontendPath, 'index.html'))
137137+ } else {
138138+ res.status(404).json({ error: 'API endpoint not found' })
139139+ }
140140+ })
141141+ } else {
142142+ logger.warn(`Frontend build not found at: ${frontendPath}`)
143143+ app.use('*', (_req, res) => {
144144+ res.sendStatus(404)
145145+ })
146146+ }
161147 }
162148163149 // Use the port from env (should be 3001 for the API server)