Self-Hosting Guide
BibMedEd runs as a set of Docker containers. You need Docker (with Compose) and Git.
Never used Docker? Install Docker Desktop for your OS (Mac/Windows/Linux — free for academic and non-commercial use). After installing, open Docker Desktop once so it can start its background service, then proceed with the Quick Start below. The whole BibMedEd stack runs locally inside the containers — no cloud account, no external services. Git can be installed from git-scm.com.
Want to look around before installing anything? Read the end-to-end case study — it walks through a real research question with screenshots and a sample methodology log so you can decide if BibMedEd fits your workflow before committing to the install.
Quick Start
After the containers finish starting (usually 30-60 seconds the first time), open http://localhost:3000 — that's BibMedEd. The interactive API docs are at http://localhost:8000/docs.
This starts five services:
| Service | Port | Description |
|---|---|---|
| Frontend | localhost:3000 |
Next.js web interface |
| API | localhost:8000 |
FastAPI backend |
| Worker | — | Celery task processor |
| PostgreSQL | localhost:5432 |
Database |
| Redis | localhost:6379 |
Message broker + cache |
The database schema is created automatically on first startup.
Security model
BibMedEd has no built-in authentication
BibMedEd is designed as a single-tenant, self-hosted tool for one research team — there is no login, no user accounts, and no per-project access control anywhere in the API. Every endpoint (list projects, run searches, read or export publications, bulk-exclude records, delete a project) trusts the numeric ID in the URL with no notion of an "owner." That's a reasonable tradeoff for a tool that runs on localhost or inside a lab's private network, but it means anyone who can reach the API has full read/write access to every project — including via the one-click "Deploy to Cloud" button, which provisions a publicly reachable *.onrender.com URL by default with no credentials required.
Before exposing BibMedEd to the public internet, do one of the following:
- Keep it private (recommended) — run it on
localhostor inside a VPN/private network, which is the defaultdocker compose upsetup. Don't forward ports 3000/8000 to the public internet. - Restrict by IP — Render's Inbound IP Rules (Scale/Enterprise plans) let you allowlist your lab's IP range for the
bibmeded-apiandbibmeded-frontendweb services so only your team can reach them. - Put an auth proxy in front — add HTTP basic auth with a reverse proxy such as Caddy's
basicauthdirective or nginx'sauth_basic, so every request needs a shared password before it reaches BibMedEd.
Treat an unprotected BibMedEd deployment the same way you'd treat an admin panel with no login screen — because that's exactly what it is.
Public read-only demo
Setting BIBMEDED_READ_ONLY=true turns an instance into a public demo that visitors can explore without installing anything, and that they cannot change.
When the flag is on:
- Every non-
GET/HEAD/OPTIONSrequest is rejected with403and{"detail": "...read-only public demo...", "read_only": true}. This is enforced by a middleware that runs before routing, so it also covers endpoints added in future releases and paths that don't exist. WebSocket connections are closed with code1008. Blocked today: creating, renaming, or deleting projects; creating the sample project; triggering searches; bulk or single exclusion; and running analyses. - No outbound API calls can be triggered. Searches (PubMed, OpenAlex, CrossRef, Semantic Scholar, Lens.org) and iCite enrichment only ever run inside a Celery task dispatched by
POST /search, which is blocked. A read-only deploy therefore needs no Redis and no worker, and/api/readyreports"redis": "skipped". - The database refuses writes from requests, as a second line of defence. Request sessions use a separate engine whose connections the database itself holds read-only: Postgres connections start with
default_transaction_read_only=on(merged with any?options=already inBIBMEDED_DATABASE_URL), and SQLite connections runPRAGMA query_only = ON. The setting is re-applied every time a connection is taken from the pool, and startup fails unless a probe (SHOW transaction_read_only=on, orPRAGMA query_only=1) confirms the guard is active. Any write path (ORM flush, Coreinsert/update/delete,Query.update, raw SQL, bulk helpers) fails; ORM flushes are reported as403, other paths as a logged500. Other database backends are refused outright. The startup seeder uses the normal, writable engine.GET /search/{query_id}still reports a stale running search asfailed, but no longer persists that change. - The bundled sample project is seeded on startup (if missing), and all six analyses are precomputed so the dashboard has data even though
POST /analysis/{type}is blocked. Seeding is idempotent and fails the startup loudly if the database is unreachable. - All exports keep working (CSV, RIS, Web of Science plain text, JSON, PRISMA SVG, methodology log, bundle zip) because they are
GETdownloads. GET /api/configreturns{"read_only": true}; the web UI uses it to show a "Read-only demo" banner and hide or disable create, delete, search, exclusion and analysis-run controls. If the endpoint can't be reached, the UI assumes read-only.- Startup logs
BibMedEd mode: read_only=<true|false>and warns about anyBIBMEDED_*environment variable that matches no setting (for exampleBIBMEDED_READONLY).
Run the demo locally
One command, no Postgres, Redis or worker:
Open http://localhost:3100 (API docs at http://localhost:8100/docs). The API seeds the bundled synthetic sample into a disposable SQLite file inside the container on every start. The ports differ from the main docker-compose.yml, so both stacks can run side by side. Stop and discard everything with docker compose -f docker-compose.demo.yml down -v.
Host a free public demo on Render
deploy/render-demo.yaml is a separate Blueprint (the root render.yaml is for self-hosting and does not create demo services). It defines two free web services and no database, Redis or worker.
- In the Render dashboard choose New → Blueprint, select your fork of the repository, and set Blueprint Path to
deploy/render-demo.yaml. - Apply it. If Render gives the services URLs other than
https://bibmeded-demo-api.onrender.comandhttps://bibmeded-demo-frontend.onrender.com(a taken name gets a suffix), edit the two literal URLs in the file (BIBMEDED_CORS_ORIGINSandNEXT_PUBLIC_API_URL) and redeploy. The API URL is baked into the frontend at build time, so changing it needs a rebuild. - Run the smoke check from the deploy checklist below.
Free web services sleep when idle, so the first visit after a quiet period waits roughly a minute for a cold start, and the demo data is rebuilt each time. The data is the bundled synthetic sample, not real research data. SQLite needs no SELECT-only role because the file is disposable and rebuilt on boot.
Deploy checklist
- Smoke-check every deploy:
curl -fsS https://<demo-api>/api/configmust print{"read_only":true}, andcurl -s -o /dev/null -w '%{http_code}' -X POST https://<demo-api>/api/projectsmust print403. Fail the deploy otherwise. - Optionally deny non-
GET/HEAD/OPTIONSmethods at the proxy too, so a misconfigured app is still protected. For example, in nginx:limit_except GET HEAD OPTIONS { deny all; }; in Caddy:@write not method GET HEAD OPTIONSfollowed byrespond @write 403. - Add a per-IP rate limit at the proxy (see below).
Run the demo against its own database — never point a read-only instance at a database that holds real projects, since every project in it becomes publicly readable.
Residual risks
Read-only mode stops mutation and outbound calls; it does not make a public instance hardened.
- Denial of service. Every read still hits the database, and
/export/bundlebuilds a zip in memory. Put a reverse proxy or CDN in front with a per-IP rate limit (for example nginxlimit_req zone=bibmeded burst=20at around 5 requests/second, or Caddy'srate_limit, or Cloudflare rate-limiting rules), plus request-size and timeout limits. - No rate limiting in BibMedEd itself. The application does not throttle clients; that has to happen at the proxy.
- Everything in the database is public. Only seed demo data.
- Interactive API docs remain available at
/docs. "Try it out" on a write endpoint just returns403, but you can hide the docs at the proxy if you prefer. - Configuration mistakes. The flag defaults to
false; if the environment variable is missing the instance is fully writable. A misspelledBIBMEDED_*name is logged as a warning at startup, but the deploy smoke check above is the real safeguard. - The database guard can be lifted by SQL that asks for it. A statement such as
SET TRANSACTION READ WRITEorPRAGMA query_only = OFFwould undo the guard for its connection until that connection goes back to the pool, when it is re-applied. BibMedEd never issues one, and no endpoint accepts SQL, but for a stronger boundary give the demo API a database role with onlySELECTprivileges (seed with a separate writable role first).
Configuration
Copy .env.example to .env to customize:
| Variable | Default | Description |
|---|---|---|
BIBMEDED_PUBMED_API_KEY |
(empty) | Optional. Register free at NCBI. With a key, PubMed requests run at 10 req/s; without one, at 3 req/s |
BIBMEDED_NCBI_EMAIL |
(empty) | Optional contact email sent to NCBI on every PubMed search/fetch request. Requests identify the tool as bibmeded; blank email and API-key values are omitted. |
BIBMEDED_PUBMED_RATE_LIMIT |
(empty) | Optional. PubMed requests per second. Empty means 10 with a key and 3 without. A value here must be a positive finite number, always wins and is not capped, so stay within NCBI's limits |
BIBMEDED_OPENALEX_EMAIL |
(empty) | Optional. Sent to OpenAlex as mailto= |
BIBMEDED_CROSSREF_EMAIL |
(empty) | Optional. Sent to CrossRef as mailto= |
BIBMEDED_SEMANTIC_SCHOLAR_API_KEY |
(empty) | Optional. Semantic Scholar keyed tier (1 req/s sustained) |
BIBMEDED_LENS_API_KEY |
(empty) | Required only for Lens.org searches. Scholarly API bearer token |
BIBMEDED_READ_ONLY |
false |
Public demo mode — see Public read-only demo |
POSTGRES_USER |
bibmeded |
Database username |
POSTGRES_PASSWORD |
bibmeded |
Database password |
POSTGRES_DB |
bibmeded |
Database name |
All defaults work out of the box — no .env file is required. docker-compose.yml passes the BIBMEDED_* source settings above into both the api and worker containers; BIBMEDED_READ_ONLY goes to the api only. The PubMed rate a search used is recorded in its methodology step and printed in the methodology export as "Request rate limit".
Stopping
Resetting the Database
To wipe all data and start fresh:
The -v flag removes the PostgreSQL data volume.
Development Setup
For contributors, the docker-compose.override.yml file automatically enables:
- Hot-reload on Python file changes
- Source code volume mounts
To disable dev mode (e.g., for local production testing), rename or remove the override file:
Health Check
Verify the API is running:
Liveness and readiness probes
For Docker / Kubernetes / Render and any orchestrator that needs a deep healthcheck, BibMedEd exposes two split probes:
GET /api/live— liveness. Returns{"status": "alive"}with no I/O. Suitable for the docker-composehealthcheck.teston theapiservice and the KuberneteslivenessProbe. Never fails as long as the process is up.GET /api/ready— readiness. Pings Postgres (SELECT 1) and Redis (PING). Returns200 {"status":"ready","checks":{"db":"ok","redis":"ok"}}when both are reachable,503 {"status":"not_ready","checks":{...}}otherwise. Use as the KubernetesreadinessProbeor the load balancer healthcheck.
Example docker-compose snippet:
services:
api:
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/api/live"]
interval: 30s
timeout: 5s
retries: 3
Every API response includes an X-Request-ID header (auto-generated unless the client supplies one). When filing a bug, include this id so logs can be correlated end-to-end.