React/Vite frontend with an API backed by PostgreSQL and Neo4j β available as either an Express backend or a FastAPI backend (same routes, same databases, pick one). Officer accounts are cookie/JWT authenticated, and an admin panel is used to provision new officer accounts.
- Project structure
- Set up β step by step (new machine, full stack)
- Run locally β quick start
- Authentication & accounts
- Per-officer data
- Groq AI summaries
- Environment variables
- Checks
- FastAPI alternative
Criminal Network Analysis/
βββ express-backend/
β βββ middleware/ # Auth guards (requireAuth, requireAdmin)
β βββ neo4j/ # Neo4j scripts and queries
β βββ routes/ # Express route handlers
β βββ scripts/ # CLI utilities (e.g. add-officer)
β βββ services/ # Business logic, incl. Groq integration
β βββ sql/ # Reference SQL for hand-managed setup
β βββ uploads/ # Uploaded PDFs on disk (gitignored)
β βββ utils/ # Shared backend helpers
β βββ .env # Local environment variables (gitignored)
β βββ .env.example # Template for required env vars
β βββ db.js # PostgreSQL connection
β βββ neo4jDriver.js # Neo4j driver setup
β βββ server.js # Express app entry point
β
βββ backend/
β βββ middleware/, routes/, services/, sql/, utils/ # Python equivalents of the above
β βββ neo4j/ # Same Neo4j scripts, shared reference with express-backend/
β βββ uploads/ # Uploaded PDFs on disk (gitignored)
β βββ tests/ # pytest suite, incl. fixtures asserting parity with Express
β βββ .env # Local environment variables (gitignored) β separate from express-backend/.env
β βββ .env.example # Template for required env vars
β βββ db.py # PostgreSQL connection
β βββ neo4j_driver.py # Neo4j driver setup
β βββ app.py # FastAPI app factory
β βββ server.py # FastAPI entry point (`python -m backend.server`)
β
βββ frontend/
β βββ public/ # Static assets (images, etc.)
β βββ src/
β β βββ api/ # API client functions
β β βββ components/ # Reusable React components
β β βββ pages/ # Route-level page components
β βββ index.html
β βββ vite.config.js
β
βββ node_modules/ # Installed dependencies (gitignored)
βββ .env # Root-level environment variables (gitignored)
βββ .gitignore
βββ .oxlintrc.json
βββ package-lock.json
βββ package.json
βββ README.md
| Folder | What's in it |
|---|---|
frontend/ |
React source (src/), static assets (public/), HTML entry point, and Vite configuration. Builds output to frontend/dist/. |
express-backend/ |
Express server, routes, middleware, services, utilities, database connections, and Neo4j scripts (neo4j/). |
backend/ |
FastAPI equivalent of the above, same API routes and database schemas. See backend/README.md for its own setup/run/verify instructions. |
Shared npm dependencies, scripts, and lint configuration live at the repository root. Run all commands below from the root unless noted otherwise.
Full walkthrough for a new machine: FastAPI backend (
backend/), React frontend, and the local AI pipeline (spaCy + Ollama). Commands are for macOS; Windows/Linux differences are called out inline. Run every command from the repository root unless a step says otherwise.
| # | Task | Command |
|---|---|---|
| 1 | Commit & push the code, or copy the whole project folder β uncommitted work isn't in Git. | β |
| 2 | Copy backend/.env privately (USB drive, password manager). It holds passwords and the JWT secret. Never commit it. |
β |
| 3 | Export the PostgreSQL database. The app needs the existing persons, cases, locations and crime_types tables; the backend does not create them. |
pg_dump -Fc -d criminal_network -f criminal_network.dump |
| 4 | Copy the custom spaCy model if you use it β spacy_crime_multientity_ner_package/output/model-best (~420 MB, gitignored). You can also retrain it in Step 7, or use a standard spaCy model instead. |
β |
| 5 | Uploaded files are stored in the database, so they move with the dump. Documents uploaded before that change may exist only in backend/uploads/ β copy those in first. |
backend/.venv/bin/python -m backend.scripts.migrate_uploads |
| 6 | Neo4j: if NEO4J_URI starts with neo4j+s://β¦databases.neo4j.io (Neo4j Aura, cloud), the new laptop reuses the same credentials β nothing to copy. |
β |
| Tool | Version | macOS (Homebrew) | Windows / Linux |
|---|---|---|---|
| Git | any | brew install git |
git-scm.com / package manager |
| Node.js | 22.12+ | brew install node |
nodejs.org installer |
| Python | 3.11β3.13 (tested on 3.13) | brew install python@3.13 |
python.org (tick "Add to PATH") |
| PostgreSQL | 16+ | brew install postgresql@16 && brew services start postgresql@16 |
postgresql.org installer / apt install postgresql |
| Tesseract OCR | any | brew install tesseract |
UB Mannheim build (Windows) / apt install tesseract-ocr |
| Ollama | latest | download from ollama.com | ollama.com |
Verify everything installed correctly:
node --version && python3 --version && psql --version && tesseract --version && ollama --versiongit clone https://github.com/Aakarsh1506/Criminal-Network-Analysis.git
cd Criminal-Network-Analysis
npm installpython3 -m venv backend/.venv
source backend/.venv/bin/activate # Windows: backend\.venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -r backend/requirements.txtThis installs FastAPI, spaCy, and the small English model en_core_web_sm. Keep this terminal's environment activated for the backend steps below.
createdb criminal_network
pg_restore --no-owner -d criminal_network criminal_network.dumpMissing
postgisextension orgeometrytype? Install PostGIS (brew install postgis), runpsql -d criminal_network -c "CREATE EXTENSION postgis;", then repeat the restore into a freshly created database. On first start, the backend adds its own tables (officers, documents, extraction and workspace tables) frombackend/sql/.
-
Aura (cloud): nothing to do β reuse the URI, user, and password from the old
backend/.env. -
New empty Neo4j database: in Neo4j Browser, run in this order:
backend/neo4j/schema.cypherbackend/neo4j/import_data.cypherbackend/neo4j/synthetic_relationships.cypher(demo links only)
The account must be allowed to create constraints.
Start the Ollama app (or run ollama serve), then:
ollama pull qwen3:1.7b # relationship extraction and investigator answers
ollama pull embeddinggemma # document search for the AI investigatorπ‘
qwen3:4bgives better answers but is slower. If your laptop has 16 GB+ RAM, pull it instead and setOLLAMA_MODEL=qwen3:4b.
Pick one:
| Option | How |
|---|---|
| Copied custom model | Place the folder at spacy_crime_multientity_ner_package/output/model-best |
Retrain it (~10β30 min on CPU, downloads en_core_web_lg) |
bash spacy_crime_multientity_ner_package/train_model.sh |
| Standard model, no training (more accurate) | python -m spacy download en_core_web_lg |
| Standard model, no training (already bundled) | keep en_core_web_sm |
Copy the old laptop's backend/.env, or start from the template:
cp backend/.env.example backend/.envThen check these values:
# PostgreSQL on this laptop β or instead set one URL, e.g. from Render:
# DATABASE_URL=postgresql://user:password@host:5432/criminal_network
PGHOST=localhost
PGPORT=5432
PGUSER=your_mac_or_postgres_user
PGPASSWORD=
PGDATABASE=criminal_network
# Neo4j (same as before for Aura)
NEO4J_URI=neo4j+s://your-instance-id.databases.neo4j.io
NEO4J_USER=your_user
NEO4J_PASSWORD=your_password
# Local AI pipeline
EXTRACTION_PROVIDER=ollama
EXTRACTION_MODE=hybrid
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen3:1.7b
OLLAMA_TIMEOUT=180
OLLAMA_MAX_TOKENS=4096
RAG_EMBEDDING_MODEL=embeddinggemma
# spaCy: an ABSOLUTE path on THIS laptop, or en_core_web_lg / en_core_web_sm
SPACY_MODEL=/Users/you/path/to/Criminal-Network-Analysis/spacy_crime_multientity_ner_package/output/model-best
# Auth: keep the old JWT_SECRET to keep sessions compatible, or generate a new one with
# python3 -c "import secrets; print(secrets.token_hex(48))"
JWT_SECRET=replace_with_a_long_random_string
ADMIN_USERNAME=admin
ADMIN_PASSWORD=choose_a_strong_password
PORT=5050
FRONTEND_ORIGIN=http://localhost:3000
NODE_ENV=development
β οΈ SPACY_MODELis the setting most often wrong after a move β the old laptop's absolute path does not exist on the new one.
The admin account (ADMIN_USERNAME / ADMIN_PASSWORD) can create officers from /admin. Or, with the virtual environment active:
python -m backend.scripts.add_officer --username jdoe --name "Jane Doe" --org "Nandipur Police" --dob 1990-05-14It prompts for the password.
Use two terminals, both in the repository root, with Ollama running:
# Terminal 1 β backend (port 5050)
source backend/.venv/bin/activate # Windows: backend\.venv\Scripts\activate
python -m backend.server# Terminal 2 β frontend (port 3000)
npm run devOpen http://localhost:3000 and log in. The first document extraction is slower while spaCy and the Ollama model load.
Upload docs/sample_database_export.sql on the Upload page with source Database export. Its
synthetic rows become 15 records and 10 relationships in the review screen; nothing is saved until
you confirm. Statements in an uploaded file are read as data and never executed.
curl http://localhost:5050/api/health # {"ok":true}
python -m pytest backend/tests -q # backend tests; no database or AI provider needed| Symptom | Fix |
|---|---|
Hybrid extraction needs spaCy and its model |
SPACY_MODEL path is wrong or the model isn't installed (Step 7β8). Restart the backend. |
Ollama model not found |
Run ollama pull for the model named in OLLAMA_MODEL. |
Unable to reach Ollama |
Open the Ollama app or run ollama serve. |
Install Tesseract on the server |
Install Tesseract (Step 1) and restart the backend. |
Document embeddings are unavailable |
ollama pull embeddinggemma, or leave RAG_EMBEDDING_MODEL empty to use keyword search only. |
| Login fails for everyone | Wrong database or JWT_SECRET; check backend/.env and restart. |
| Port 5050 or 3000 already in use | Stop the other process (lsof -i :5050) or change PORT in backend/.env and the proxy target in frontend/vite.config.js. |
| Graph pages show connection errors | Check NEO4J_URI, user, and password; Aura instances pause when idle and must be resumed in the Aura console. |
.env changes have no effect |
The backend reads .env only at start; stop and restart it. |
Quick-start version of the setup above β use this if you already have prerequisites and databases in place.
Use Node.js 22.12 or newer. Install dependencies with:
npm installCreate express-backend/.env using express-backend/.env.example if you don't already have it, then configure your PostgreSQL and Neo4j connections (see Environment variables for the full list).
Run the frontend and backend in separate terminals:
npm run server # starts the Express APInpm run dev # starts the Vite dev serverOpen http://localhost:3000.
Same frontend, same port, same databases β only the backend terminal command changes. Requires Python 3.11+.
- Create and activate a virtual environment for it, then install dependencies:
python3 -m venv backend/.venv source backend/.venv/bin/activate # Windows: backend\.venv\Scripts\activate python -m pip install -r backend/requirements.txt
- Create
backend/.envusingbackend/.env.exampleif you don't already have it, then configure it β this is a separate file fromexpress-backend/.env, even if you're pointing both at the same databases. - With the virtual environment still active, start the API:
npm run server:fastapi # starts the FastAPI API (python3 -m backend.server)npm run dev # starts the Vite dev server, same as above
Open http://localhost:3000 β same URL either way. See backend/README.md for the full setup/run/verify reference, including the add-officer CLI equivalent (npm run add-officer:fastapi).
Every API route except /api/auth/login and /api/health requires a valid session cookie (requireAuth). There are two kinds of accounts:
Stored in the officers table, password hashed with bcrypt. Created either:
- Through the admin panel (
/admin, see below) β the normal path for a real org. - Via the CLI script, useful for bootstrapping the very first account on a fresh deployment:
npm run add-officer -- --username jdoe --password "SomeStrongPass!" --name "Jane Doe" --org "Delhi Police"
An officer's session is scoped to their officer_id everywhere it matters β pinned criminal, working list, and uploaded documents are all private to that officer (see Per-officer data).
A single hardcoded account, checked against ADMIN_USERNAME / ADMIN_PASSWORD in your backend's .env (express-backend/.env or backend/.env, depending which backend you're running) β not a row in the officers table. Logging in with these credentials on the normal login page redirects to /admin instead of /dashboard.
From /admin you can:
- Create new officer accounts (username, password, name, DOB, org).
- View all existing officers and deactivate one (soft-delete via
is_active, not a hard delete β keeps history intact).
β οΈ This is a stopgap for one deployment/demo. It's a single shared password with no rotation and no audit trail. Before a real multi-org deployment, this should become a properrole = 'admin'row inofficers(bcrypt-hashed like everyone else), ideally with a separate "platform" tier above it that provisions each org's first admin β see project notes for the fuller plan.
Three things are scoped to the logged-in officer's officer_id, each backed by its own self-creating table (no manual migration needed β they're created with CREATE TABLE IF NOT EXISTS the first time the backend boots):
| Feature | Table | Notes |
|---|---|---|
| Pinned criminal (Dashboard) | officer_pinned_criminal |
One row per officer, replaced on re-pin |
| Working list (Dashboard / Criminal List) | officer_working_list |
Many rows per officer |
| Uploaded documents (Upload page) | officer_documents |
Files stored on disk in express-backend/uploads/ (or backend/uploads/ for FastAPI), metadata + ownership in Postgres |
None of this is shared across officers β each of these used to be localStorage-based (or in-memory, for documents) and was visible to whoever was using the browser, not the logged-in officer. All three now go through authenticated, officer-scoped API routes instead.
- Create an API key at console.groq.com/keys.
- Add
GROQ_API_KEY=your_keyto your backend's.env(express-backend/.envorbackend/.env). Keep existing database settings. - Restart the backend (
npm run serverornpm run server:fastapi). - Open a profile and click Explain this network below its graph.
The optional GROQ_MODEL setting defaults to openai/gpt-oss-20b. The Express backend calls the Groq Chat Completions API using Node's built-in fetch; the FastAPI backend uses httpx. Neither needs an additional SDK.
β οΈ Never place the key in frontend code or aVITE_variable. Backend.envfiles are ignored by Git.
- Requires a valid officer/admin session, same as the rest of the API.
- Reads records from the databases and sends the profile ID/name/status, up to 50 cases, and 25 graph overlap rows to Groq.
- Returns
{ explanation }. - Photos and demographic fields are excluded.
- Generation happens only on a button click.
- The response explains shared attributes, not proven personal associations or guilt.
- Empty overlap results may mean Neo4j was unavailable. Summaries require source verification.
Limits & safety:
- Requests have a 30-second Groq timeout and a process-wide limit of three concurrent summaries.
- Missing keys, provider failures, and usage limits produce actionable UI errors.
The FastAPI backend (backend/.env.example) uses the same variable names β copy whichever backend's example file you're running to a real .env in that same folder. Each backend reads its own .env; they don't share one.
PGHOST=localhost
PGPORT=5432
PGUSER=postgres
PGPASSWORD=your_password_here
PGDATABASE=criminal_network
PORT=5050
NEO4J_URI=neo4j+s://your-instance-id.databases.neo4j.io
NEO4J_USER=neo4j
NEO4J_PASSWORD=your_aura_password_here
# Groq AI summaries (server only)
GROQ_API_KEY=
GROQ_MODEL=openai/gpt-oss-20b
# Auth
# Generate a real secret with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
JWT_SECRET=replace_with_a_long_random_string
JWT_EXPIRES_IN=12h
COOKIE_NAME=cna_token
# Set to your deployed frontend URL in production (dev works out of the box via the Vite proxy)
FRONTEND_ORIGIN=http://localhost:3000
NODE_ENV=development
# Admin panel login β a single hardcoded account, not stored in the
# officers table. Change these before any real deployment; there's no
# UI to change them yet, only this file.
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change_this_admin_password| Scenario | What to do |
|---|---|
| Frontend/backend on different domains | Set NODE_ENV=production so cookies get secure: true, and set sameSite to "none" in express-backend/routes/auth.js's COOKIE_OPTIONS β "lax" (the local-dev default) gets silently dropped cross-domain. Point FRONTEND_ORIGIN at your exact deployed frontend URL; CORS needs it to match exactly. |
| Managed Postgres provider (e.g. Render) | The FastAPI backend accepts a single DATABASE_URL in backend/.env; when set, it replaces PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE. Use Render's External Database URL when the backend runs elsewhere, or the Internal Database URL when the backend is a Render service in the same region. SSL is tried first by default; append ?sslmode=require to insist on it. To move existing data: pg_dump (matching your local server's major version) then pg_restore --no-owner --no-acl -d "$DATABASE_URL" criminal_network.dump. |
| Neo4j by raw IP instead of a domain | Public CAs won't issue certificates for bare IPs, so you'll be stuck with a self-signed certificate. Node's TLS stack may tolerate that as-is, but Python's ssl module won't β backend's driver needs its URI scheme rewritten from neo4j+s:///bolt+s:// to neo4j+ssc:///bolt+ssc:// to skip certificate verification. Prefer a real domain + CA-signed cert (or Neo4j Aura, which provides one automatically) wherever possible instead. |
npm run build
npm run lint
node --test express-backend/services/groq.test.jsFor the FastAPI backend, see backend/README.md for its pytest/ruff equivalents.
The Python backend is in backend/. It preserves the existing API routes and frontend proxy port. Follow its setup instructions, activate backend/.venv, then run npm run server:fastapi from the repository root.