This guide covers setup, deployment, and common development tasks for PostScholar.
- Node.js 18 or higher
- PostgreSQL 14 or higher
- npm (comes with Node.js)
- Git
- Clone and install dependencies:
git clone https://github.com/PostScholar/postscholar.git
cd postscholar
npm install
cd server && npm install
cd ../client-next && npm install
cd ..- Set up PostgreSQL database:
# Create database
createdb postscholar
# Or using psql:
psql postgres
CREATE DATABASE postscholar;
\q- Configure environment variables:
cp server/.env.example server/.env
# Edit server/.env with your valuesSee server/.env.example for the full list. Minimum for local dev:
server/.env:
DATABASE_URL=postgresql://localhost:5432/postscholar
JWT_SECRET=your-dev-secret-key-change-in-production
CLIENT_URL=http://localhost:3001
ORCID_CLIENT_ID=your-orcid-client-id
ORCID_CLIENT_SECRET=your-orcid-client-secret
PORT=3000ORCID redirect is derived from CLIENT_URL ({CLIENT_URL}/orcid/callback).
Google and GitHub callbacks also use the frontend URL ({CLIENT_URL}/auth/google/callback and {CLIENT_URL}/auth/github/callback).
client-next/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:3000- Run database migrations:
npm run migrate- Start development servers:
# Run both frontend and backend together:
npm run dev
# Or run separately in different terminals:
npm run dev:server # http://localhost:3000
npm run dev:client # http://localhost:3001Local development:
npm run migrateRemote (Railway):
# Recommended: from server/ directory
cd server
npm run migrate:railway
# Alternative: Railway CLI from repo root
railway run node server/db/migrate.js
# Alternative: direct connection string
cd server && DATABASE_URL="postgresql://..." node db/migrate.jsMigration state is tracked in the migrations table (not schema_migrations).
- Create a new SQL file in
server/db/migrations/:
touch server/db/migrations/016_your_migration_name.sql- Write idempotent SQL:
-- Use IF NOT EXISTS for safety
CREATE TABLE IF NOT EXISTS new_table (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_new_table_created ON new_table(created_at DESC);- Run the migration:
npm run migrate- Always use
IF NOT EXISTSfor tables, indexes, and constraints - Use
ALTER TABLE ... ADD COLUMN IF NOT EXISTSfor new columns - Test migrations locally before running in production
- Make migrations reversible when possible
- Add comments explaining complex migrations
# Drop and recreate database
dropdb postscholar
createdb postscholar
# Run all migrations
npm run migratePostScholar uses Vercel for frontend hosting.
-
Connect repository to Vercel:
- Go to vercel.com
- Import your GitHub repository
- Set root directory to
client-next
-
Configure environment variables in Vercel:
NEXT_PUBLIC_API_URL→ Your Railway backend URLAPI_URL→ Same Railway backend URL for server-side rendering and/apirewrites
-
Deploy:
- Vercel automatically deploys on push to main
- Preview deployments for all PRs
PostScholar uses Railway for backend hosting.
-
Connect repository to Railway:
- Go to railway.app
- Create new project from GitHub repo
- Set root directory to
server
-
Add PostgreSQL database:
- Click "New" → "Database" → "PostgreSQL"
- Railway automatically sets
DATABASE_URL
-
Configure environment variables:
JWT_SECRET→ Generate secure secretCLIENT_URL→ Your Vercel URL (https://postscholar.org)ORCID_CLIENT_ID→ From ORCID developer consoleORCID_CLIENT_SECRET→ From ORCID developer consoleGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET→ From Google Cloud OAuth credentialsGITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET→ From GitHub OAuth app settingsNODE_ENV→production(enables stricter rate limits)PORT→ 3000 (Railway sets this automatically)RESEND_API_KEY/EMAIL_FROM→ For password reset and verification emails
-
Run migrations on Railway:
cd server
npm run migrate:railway- Promote a moderator (after
018_user_roles.sql):
UPDATE users SET role = 'moderator' WHERE username = 'yourusername';Run in Railway Postgres → Query. Then sign out and back in on postscholar.org. Open /moderation.
- Deploy:
- Railway automatically deploys on push to main
- Monitor logs in Railway dashboard
- Register application at https://orcid.org/developer-tools
- Set redirect URI to
{CLIENT_URL}/orcid/callback(for example,https://postscholar.org/orcid/callback) - Add credentials to environment variables
- Create route in server:
// server/routes/example.js
const express = require('express')
const router = express.Router()
const pool = require('../db')
const authenticateToken = require('../middleware/authenticateToken')
router.get('/example', authenticateToken, async (req, res) => {
try {
const result = await pool.query('SELECT * FROM example WHERE user_id = $1', [req.user.userId])
res.json({ data: result.rows })
} catch (err) {
console.error('GET /example error:', err)
res.status(500).json({ error: 'Internal server error' })
}
})
module.exports = router- Register route in server/index.js:
app.use('/example', require('./routes/example'))- Add client function in client-next/src/lib/api.js:
export function getExample() {
return api.get('/example')
}- Use in component:
import { getExample } from '@/lib/api'
const data = await getExample()- Create page file:
touch client-next/src/app/example/page.js- Add page content:
export const metadata = {
title: 'Example — PostScholar'
}
export default function ExamplePage() {
return <div>Example</div>
}# Server tests
cd server
npm test
# Client tests (when added)
cd client-next
npm test# Client
cd client-next
npm run lint
npm run build # Includes type checking
# Server
cd server
npm run lintBefore deploying major changes:
- Test auth flow (register, login, logout)
- Test paper lookup with valid DOI
- Test discussion creation
- Test commenting and replies
- Test ORCID verification
- Test bookmarks
- Test profile editing
- Test rate limiting (try 15+ rapid requests)
- Test error boundaries (trigger errors)
- Test 404 page (visit invalid URL)
# Health check
curl http://localhost:3000/health
# Login
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password"}' \
-c cookies.txt
# Protected endpoint
curl http://localhost:3000/auth/me \
-b cookies.txt# Find process using port 3000
lsof -i :3000
# Kill process
kill -9 <PID># Check PostgreSQL is running
pg_isready
# Check DATABASE_URL
echo $DATABASE_URL
# Test connection
psql $DATABASE_URL -c "SELECT 1"# Check which migrations have run
psql $DATABASE_URL -c "SELECT * FROM migrations ORDER BY id"
# Manual rollback (if needed) — migrations are forward-only; fix forward with a new SQL file
psql $DATABASE_URL
DELETE FROM migrations WHERE filename = '016_problematic.sql';
\q
# Re-run migrations
npm run migrateEnsure CLIENT_URL in server/.env matches your frontend URL exactly:
CLIENT_URL=http://localhost:3001 # Local
CLIENT_URL=https://postscholar.org # Production# Clear Next.js cache
cd client-next
rm -rf .next
# Rebuild
npm run build- Check that
credentials: trueis set in CORS config - Ensure frontend uses
credentials: 'include'in fetch - Verify cookie is httpOnly and secure in production
- Check that domains match (no cross-origin issues)
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@host:5432/db |
JWT_SECRET |
Secret key for JWT signing | your-secret-key |
CLIENT_URL |
Frontend URL for CORS | http://localhost:3001 |
NODE_ENV |
Runtime environment; production enables stricter defaults | production |
ORCID_CLIENT_ID |
ORCID OAuth client ID | From ORCID developer console |
ORCID_CLIENT_SECRET |
ORCID OAuth client secret | From ORCID developer console |
GOOGLE_CLIENT_ID |
Google OAuth client ID | From Google Cloud |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | From Google Cloud |
GITHUB_CLIENT_ID |
GitHub OAuth client ID | From GitHub developer settings |
GITHUB_CLIENT_SECRET |
GitHub OAuth client secret | From GitHub developer settings |
RESEND_API_KEY |
Resend API key for password reset and verification email | From Resend |
EMAIL_FROM |
Sender for transactional email | PostScholar <noreply@postscholar.org> |
SENTRY_DSN |
Optional Sentry DSN for error tracking | From Sentry |
RATE_LIMIT_AUTH_MAX |
Optional auth rate limit override per 15 minutes | 10 |
RATE_LIMIT_GENERAL_MAX |
Optional general rate limit override per 15 minutes | 100 |
PORT |
Server port | 3000 |
| Variable | Description | Example |
|---|---|---|
NEXT_PUBLIC_API_URL |
Backend API URL | http://localhost:3000 |
API_URL |
Optional server-side/proxy backend API URL | http://localhost:3000 |