The cBioPortal Data Contribution Dashboard makes it easy for researchers to contribute cancer genomics datasets to cBioPortal and follow their progress through the curation process.
Researchers can suggest published papers or submit their own datasets, track each submission from start to finish, and explore analytics about the data already available in cBioPortal. The curation team can review submissions, manage the curation workflow, and communicate progress in one place.
Researchers can:
- Suggest a published paper for curation.
- Submit their own dataset to cBioPortal.
- Provide a link to their data (Google Drive, Dropbox, Box, etc.) instead of uploading files directly.
- Add study details and give the curation team access to the data.
After submitting, contributors can monitor the progress of their submission through each stage of the curation pipeline, from Submitted to Released, without needing to contact the curation team for updates.
The dashboard includes interactive analytics powered by cBioPortal data, including:
- Samples by cancer type
- Growth of studies over time
- Available data types
- Submission and curation pipeline statistics
Frontend
- React + TypeScript, built with Vite
- Tailwind CSS with shadcn/ui (Radix UI primitives)
- React Router, TanStack Query
- Recharts and AG Grid for charts and tables
keycloak-jsfor authentication
Backend
- Node.js + Express
- PostgreSQL (
pg) — the app's own data (users, submissions) - ClickHouse (over HTTP) — read-only access to cBioPortal's genomics dataset for analytics
- Keycloak (OIDC) — authentication; tokens validated via JWKS (
jose) - Helmet, CORS, rate limiting, and request validation
Authentication is handled by Keycloak.
Users sign in through Keycloak, and the backend validates the JWT before creating or retrieving the corresponding user record.
The application uses two databases:
| Database | Purpose |
|---|---|
| PostgreSQL | Stores users, submissions, comments, workflow status, and other application data. |
| ClickHouse | Provides read-only access to cBioPortal genomics data used for analytics and dashboards. |
+--------------------+
| Keycloak |
| Authentication |
+---------+----------+
|
| JWT
v
+----------------+ +--------------------+
| React (Vite) | --> | Express API |
+----------------+ +---------+----------+
|
+--------------+--------------+
| |
v v
PostgreSQL ClickHouse
(Users & Submissions) (cBioPortal Analytics)
Read / Write Read Only
backend/
├── src/
│ ├── db/ # PostgreSQL and ClickHouse clients
│ ├── middleware/ # Authentication and authorization
│ ├── routes/ # API endpoints
│ └── utils/ # Helper functions
└── scripts/ # LevelDB → PostgreSQL migration (one-time)
frontend/
└── src/
├── components/ # Shared UI components
├── pages/ # Application pages
├── services/ # API client and Keycloak setup
└── hooks/ # React hooks
The application uses Keycloak with OpenID Connect (OIDC).
- Users authenticate through Keycloak.
- Keycloak returns a JWT access token.
- The frontend includes the token with each API request.
- The backend validates the token using Keycloak's JWKS endpoint.
- Once validated, the request is processed using the authenticated user's identity and roles.
The application does not store or manage user passwords.
Both services ship as container images, published to the DockerHub repo
cbioportal/curation-dashboard and distinguished by a tag suffix:
| Image | Tags | Port | Probe |
|---|---|---|---|
| Backend (Express API) | :latest-backend, :<short-sha>-backend |
5001 | GET /api/health |
| Frontend (nginx + SPA) | :latest-frontend, :<short-sha>-frontend |
8080 | GET /healthz |
.github/workflows/docker_ci.yml builds and pushes both on every push to main,
and on demand via workflow_dispatch with a ref input. Deployments should
reference the immutable <short-sha> tag — rollouts against a moving latest don't
reliably restart pods.
Images are multi-arch (linux/amd64, linux/arm64). Keep linux/arm64: the
curation EKS nodegroup these run on is Graviton (t4g.medium,
BOTTLEROCKET_ARM_64), so an amd64-only image would fail with exec format error.
# Backend
docker build -t dcd-backend ./backend
docker run --rm -p 5001:5001 \
-e KEYCLOAK_ISSUER=http://localhost:8081/auth/realms/dashboard \
-e FRONTEND_URL=http://localhost:8080 \
-e PGHOST=host.docker.internal -e PGUSER=postgres -e PGPASSWORD=... -e PGDATABASE=dashboard \
dcd-backend
# Frontend
docker build -t dcd-frontend ./frontend
docker run --rm -p 8080:8080 \
-e VITE_API_URL=http://localhost:5001 \
-e VITE_KEYCLOAK_URL=http://localhost:8081/auth \
-e VITE_KEYCLOAK_REALM=dashboard \
-e VITE_KEYCLOAK_CLIENT_ID=dashboard-frontend \
dcd-frontendVite normally freezes import.meta.env.VITE_* values into the bundle, which would
make each image environment-specific. Instead, the app reads its configuration from
window.__ENV__ via src/config.ts, and docker-entrypoint.sh rewrites
/config.js from the container environment on every start. One image is therefore
promotable across dev/staging/prod by changing a ConfigMap — no rebuild required.
Precedence is runtime (window.__ENV__) → build-time (.env) → local dev default,
so npm run dev keeps working unchanged.
Recognised variables: VITE_API_URL, VITE_KEYCLOAK_URL, VITE_KEYCLOAK_REALM,
VITE_KEYCLOAK_CLIENT_ID. To add one, extend both src/config.ts and
docker-entrypoint.sh.
- The image sets
NODE_ENV=production, which makes bothKEYCLOAK_ISSUERandFRONTEND_URLmandatory — the process exits non-zero at startup if either is missing.FRONTEND_URLis the CORS origin. initializeDatabases()appliessrc/db/schema.sql(idempotent) and throws if Postgres is unreachable, so a bad database config produces a crash-loop rather than a silently degraded pod. Give the pod astartupProbewith enough slack for Postgres to accept connections.PGSSLROOTCERTis resolved relative to the app root when it is a bare filename, so in-cluster set it to the absolute path of a mounted Secret, e.g.PGSSLROOTCERT=/etc/ssl/pg/ca.pem. Certificates are never baked into the image (*.pemis inbackend/.dockerignore).- ClickHouse is optional: without
CLICKHOUSE_HOSTthe app still boots, and only the analytics endpoints fail.
The frontend build requires Node 18+ (Vite 4); the images use node:20-alpine.
The engines.node: >=16 field in package.json understates this — building with
Node 16 or older fails.