ResearchOS is designed to run locally, but you can deploy it to a server for remote access.
cd backend
uv sync --no-dev
uv run uvicorn app:app --host 0.0.0.0 --port 8000No build step — FastAPI serves directly. For production, add --workers 2 (single user, no need for many) or put it behind gunicorn:
uv run gunicorn app:app -w 2 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000cd frontend
npm install
npm run buildThis produces frontend/dist/ — static files that any web server can serve.
The backend reads from backend/.env:
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL |
Yes | Your Supabase project URL (e.g. https://abc.supabase.co) |
SUPABASE_KEY |
Yes | Supabase publishable (anon) key |
OPENAI_API_KEY |
No | OpenAI API key. AI features disabled without it. |
PAPER_STORAGE_PROVIDER |
No | supabase (default) or gcs; controls new PDF uploads only |
GCS_PROJECT_ID |
For GCS | Google Cloud project used by the storage client |
GCS_PDF_BUCKET |
For GCS | Dedicated public-read PDF bucket; also used to recognize managed GCS URLs |
For local GCS access, run gcloud auth application-default login. CLI login alone
does not authenticate the Python client. On a hosted backend, supply an attached
service account or workload identity with bucket-level Storage Object User access.
Never expose credentials in frontend configuration. Supabase credentials remain
required for the database. Follow the PDF migration runbook
for bucket setup, CORS, copying, verified URL updates, rollback, and delayed cleanup.
For a brief storage cutover, create backend/data/migration/maintenance.flag to
return HTTP 503 for API mutations while allowing reads; remove it when finished.
The marker is local to this checkout, so coordinate every instance before using it.
- Create a Supabase project (free tier works for personal use)
- Go to SQL Editor and run
backend/migrations/schema.sql - Go to Storage and verify the
pdfsbucket exists (created by the migration) - Copy the project URL and anon key from Project Settings > API
The free tier includes 500MB database, 1GB storage, and 50,000 monthly active users (irrelevant for single-user).
Run both backend and frontend on a single VPS behind a reverse proxy.
Nginx config:
server {
listen 80;
server_name research.yourdomain.com;
# Frontend static files
location / {
root /path/to/frontend/dist;
try_files $uri $uri/ /index.html;
}
# API proxy
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Use systemd or supervisord to keep the backend running. Add HTTPS via Let's Encrypt.
No official Dockerfile yet, but the setup is straightforward:
# Backend
FROM python:3.11-slim
RUN pip install uv
WORKDIR /app
COPY backend/ .
RUN uv sync --no-dev
CMD ["uv", "run", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
# Frontend
FROM node:18-alpine AS build
WORKDIR /app
COPY frontend/ .
RUN npm install && npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf- Frontend on Vercel: point to
frontend/directory, build commandnpm run build, outputdist/. Add a rewrite rule:/api/:path*->https://your-backend.railway.app/api/:path* - Backend on Railway/Render: deploy
backend/with start commanduv run uvicorn app:app --host 0.0.0.0 --port $PORT. Set environment variables in the dashboard.
ResearchOS has no authentication. It's designed for single-user local use. If you deploy it publicly:
- Put it behind HTTP Basic Auth or a VPN
- Never expose the Supabase service role key (use the anon key)
- RLS is disabled on all tables — the anon key has full access
- Consider restricting CORS origins in
backend/app.pyto your domain
Your data lives in Supabase. Backup options:
- Supabase dashboard: Project Settings > Database > Backups (automatic daily on paid plans)
- pg_dump: Connect to the database directly and dump with
pg_dump - API export: Use the BibTeX export endpoint to export your library, or query the API for full data