Linkr
Home Resources Tools Documentation Blog Demo
FR
  • What is Linkr?
  • Deployment modes
  • Quick start
  • Local install
  • With Docker
  • Manual install
  • Client-only
  • Your first project
  • Workspaces and projects
  • The data pipeline
  • Entities and sharing
  • Versioning and collaboration
  • Overview
  • Projects
  • Wiki
  • Plugins
  • Members and roles
  • Settings
  • Schemas
  • Databases
  • Derived sub-databases
  • Data quality
  • Data catalog
  • SQL script collections
  • ETL pipelines
  • Overview
  • Mapping projects
  • Global view
  • Target concepts
  • Mapping editor
  • Suggestions
  • Evaluation
  • Export
  • Overview
  • Concepts
  • Cohorts
  • Patient data
  • Pipeline
  • Datasets
  • IDE
  • Web apps
  • Versioning
  • Overview
  • Tabs and widgets
  • Built-in widgets
  • Analysis widgets
  • Control charts (SPC)
  • Surveys and eCRF
  • R and Python code
  • Filters, settings and export
  • Overview
  • Presentation mode
  • Exporting a report
  • Agents
  • Model providers
  • Skills
  • Authoring through MCP
  • Import and export
  • Git versioning
  • Community catalog
  • Publishing content
  • Production install
  • Configuration
  • Authentication and permissions
  • Files on the server
  • Backup and restore
  • Glossary
  • Keyboard shortcuts
  • Release notes
Documentation Getting started Manual install

Manual install

Install Linkr from source with Python and Node.js: FastAPI backend and frontend, for development or to deploy without Docker.

Summary

The same features as Docker, but from source: you clone the repository, install the Node.js and Python dependencies, then one command starts the backend and the frontend together. This is the method for developing on Linkr, with hot reload.

Requirements

  • Node.js 20+ and npm (nodejs.org).
  • Python 3.12+ — check with python3 --version (python --version on Windows).
  • Git.
  • About 1.5 GB of disk space.

Installation

Every command runs from the repository root.

Clone the repository and install dependencies

git clone https://framagit.org/interhop/linkr/linkr.git
cd linkr
npm install
python3 -m venv apps/api/.venv
source apps/api/.venv/bin/activate
pip install -e "apps/api[dev]"
git clone https://framagit.org/interhop/linkr/linkr.git
cd linkr
npm install
python -m venv apps\api\.venv
apps\api\.venv\Scripts\Activate.ps1
pip install -e "apps/api[dev]"

Create the configuration files

One for the backend, one for the frontend.

cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env.local

Generate the secret key

Copy the printed value, then paste it into apps/api/.env in place of dev-secret-change-in-production.

python3 -c "import secrets; print(secrets.token_urlsafe(48))"
python -c "import secrets; print(secrets.token_urlsafe(48))"

Start Linkr

Wait for the « Uvicorn running » and « VITE ready » lines, then open http://localhost:3000 and follow the setup wizard. Ctrl+C stops everything.

npm run dev:all

You're set

Linkr is running. The sections below are there when you need them: settings, installing on a remote machine, deploying without Docker, troubleshooting.

What these steps do

  • The virtual environment (apps/api/.venv) keeps Linkr’s Python dependencies apart from the rest of your machine. For a PostgreSQL deployment, install apps/api[dev,postgres] instead of apps/api[dev].
  • apps/web/.env.local switches the app into server mode: it sets VITE_API_URL=http://localhost:8000. Without it, the frontend stays client-only.
  • The secret key signs login tokens and encrypts the passwords of registered databases. The backend refuses to start while the example key is in place. Keep it stable afterwards: changing it logs everyone out and makes already-registered passwords unreadable.
  • npm run dev:all starts the backend and the frontend in the same terminal, every log line prefixed by its source (web or api).

On Windows, in a new terminal

The virtual environment is only active in the terminal where you activated it. On macOS and Linux, npm run dev:all finds it on its own; on Windows, activate it again before starting Linkr (apps\api\.venv\Scripts\Activate.ps1).

The first-launch wizard

The very first visit shows not the login page but a three-step wizard — there is no user yet. Subsequent visits show the usual login page.

1

Database

The wizard shows the database the server is using. This screen is read-only: the database is configured server-side through LINKR_DATABASE_URL, and created automatically. To change it, edit the server configuration and restart.

2

Create Admin Account

This will be the instance's first administrator account. Enter a username and a password. In a development build the fields are pre-filled with admin / admin; a production build starts with empty fields.

3

Default data

Last step: install the demo workspace, which holds an OMOP database, concept mappings, an ETL pipeline and example projects. Click « Install and finish », or « Start empty » to begin with a blank instance. The choice is not final: content can be installed or removed later from the Catalog page.

Creating the administrator from the command line

For a scripted install, the wizard can be bypassed by calling the API directly:

curl localhost:8000/api/v1/setup/status

curl -X POST localhost:8000/api/v1/setup/initialize \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"a-strong-password"}'

The first command answers {"needs_setup": true} as long as the instance is not initialised.

Backend settings

The three settings that matter, in apps/api/.env:

  • LINKR_DATA_DIR — the folder where Linkr keeps everything: the SQLite database and the large files (Parquet, attachments, IDE files). Defaults to ~/.linkr. This is the folder you back up.
  • LINKR_DATABASE_URL — leave it commented out for SQLite inside the data folder (single-user, the default). Uncomment and set it for PostgreSQL.
  • LINKR_SECRET_KEY — signs login tokens and encrypts the passwords of registered databases. Replace it before the first start (step 3).

A fourth setting matters as soon as you leave the default ports: LINKR_CORS_ORIGINS must hold the frontend’s exact address (http://localhost:3000 in the example file). If you serve the frontend on another port, update it, or the browser will block calls to the API.

The database is created automatically on first startup, and migrations run on every launch.

Running the two processes separately

npm run dev:all is right for most cases. To follow each side’s logs on its own, or restart one without the other, open two terminals:

# Terminal 1 — backend
npm run dev:api

# Terminal 2 — frontend
npm run dev:web

Going back to client-only for a single run

Once apps/web/.env.local exists, every npm run dev:web starts in server mode. To run the frontend client-only without touching that file:

npm run dev:client

The variable is forced empty for that run. Handy for checking how a feature behaves without a backend.

Installing on a remote machine

Everything above assumes the browser and Linkr run on the same machine. If you develop inside a VS Code container, on a virtual machine or on a remote server, the browser is elsewhere: four settings, all in apps/web/.env.local, then become necessary. Without them the page stays blank or shows Blocked request.

  • WEB_HOST — by default the development server listens on localhost only, meaning the inside of the machine: nothing gets out. Set 0.0.0.0 so it accepts outside connections.
  • WEB_ALLOWED_HOSTS — the server refuses requests whose domain name it does not recognise, a protection against DNS rebinding. A proxy exposing Linkr under chu-example.org is therefore rejected with Blocked request. This host is not allowed. Give it the expected name, comma-separated if there are several; a leading dot covers subdomains (.chu-example.org).
  • BASE_PATH — set it only if the application is served under a sub-path, for instance https://chu-example.org/container-3612/, and the proxy does not strip that prefix. It then prefixes the application’s files and internal addresses.
  • WEB_PORT — the listening port, if 3000 is not the one you expose.
WEB_HOST=0.0.0.0
WEB_ALLOWED_HOSTS=chu-example.org
BASE_PATH=/container-3612/
WEB_PORT=4321

VITE_API_URL must become relative

This is the most costly mistake in this setup, because it produces no clear message. The example file holds VITE_API_URL=http://localhost:8000: an absolute address, which the browser resolves from its own machine. From a remote workstation, localhost means that workstation, not the container — the application loads, then every API call fails.

Replace it with a relative address:

VITE_API_URL=/

Calls then go through the development server, which forwards them to the backend itself. Since they leave from the same origin as the page, LINKR_CORS_ORIGINS needs no change.

If automatic reloading loops

When a proxy handles HTTPS, the page loads over https on port 443 while automatic reloading tries to connect over ws on the development port. The connection fails, indefinitely. Three variables catch it:

WEB_HMR_PROTOCOL=wss
WEB_HMR_CLIENT_PORT=443
WEB_HMR_HOST=chu-example.org

The application works without them — only reloading on each change is lost.

Finding out whether the proxy strips the prefix

BASE_PATH cannot be guessed: wrong either way, none of the application’s files load. To settle it, run a small server that prints the path it receives, on the port you expose:

python3 -c "
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
    def do_GET(s):
        s.send_response(200); s.end_headers()
        s.wfile.write(f'PATH={s.path}\nHOST={s.headers.get(\"Host\")}\n'.encode())
HTTPServer(('0.0.0.0',4321),H).serve_forever()
"

Then open the full address in your browser. If the answer shows PATH=/container-3612/, the prefix is kept: set BASE_PATH. If it shows PATH=/, the proxy stripped it: leave BASE_PATH empty. The HOST= line gives you, in passing, the exact value to put in WEB_ALLOWED_HOSTS.

These settings are for development only

WEB_HOST, WEB_ALLOWED_HOSTS and the WEB_HMR_* variables apply to the development server only. Going to production means Docker, where nginx already listens on every interface; only the sub-path is configured there, through the BASE_PATH build argument. And WEB_ALLOWED_HOSTS=true, which disables the domain check entirely, suits a private network only: it reopens the hole that check protects.

Deploying without Docker

On a server, keep three things separate: the code (the checkout), the data (a dedicated folder you back up) and the secrets (never world-readable). Docker remains the simplest route; for a system install, systemd with an environment file at chmod 600 is the recommended pattern:

sudo install -d -o linkr -g linkr /var/lib/linkr
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
# /etc/systemd/system/linkr-api.service
[Service]
User=linkr
WorkingDirectory=/opt/linkr/apps/api
EnvironmentFile=/etc/linkr/linkr.env
ExecStart=/opt/linkr/apps/api/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000

In /etc/linkr/linkr.env (at chmod 600), at a minimum: LINKR_DATA_DIR, LINKR_SECRET_KEY and LINKR_CORS_ORIGINS (the frontend’s public address — without it the browser will block calls to the API).

Back up the data folder

In server mode, everything your users produce lives in LINKR_DATA_DIR — the application database and all the files. It is the only folder to back up, but it is essential: the code repository holds no data at all.

Repository layout

The repo is a Turborepo monorepo:

  • apps/web/ — React + Vite frontend (the main app).
  • apps/api/ — FastAPI backend (Python).
  • packages/default-plugins/ — default analysis plugins (Tableau descriptif, Constructeur de graphiques, etc.).
  • packages/linkr-format/ — schemas and validator for the export format.
  • docker/ — Docker configurations.
  • docs/ — internal project documentation (different from the user documentation you’re reading here).

Useful commands

CommandEffect
npm installInstall all monorepo dependencies.
npm run dev:webStart frontend in dev mode (port 3000).
npm run dev:clientStart the frontend client-only, even if VITE_API_URL is set.
npm run dev:apiStart the FastAPI backend (port 8000).
npm run dev:allStart both at once (needs npm install at the root).
npm run data:fetchDownload the demo data into the seed folder.
npm run buildProduce production build for all workspaces.
cd apps/web && npm run previewServe the production build locally.

Troubleshooting

npm install fails. Check that you are on Node.js 20+ (node -v). If you have several Node.js versions, run nvm use 20.

python: command not found. On macOS and Linux the command is python3; on Windows it is python. Pick your system’s tab in the steps above.

pip install rejects your Python version. The backend requires Python 3.12 or later (python3 --version). On an older version the install stops on a compatibility message: install a newer Python, then recreate the virtual environment.

Windows refuses to run Activate.ps1. PowerShell blocks scripts by default. Allow them for your account with Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then run the activation again.

The backend exits on LINKR_SECRET_KEY is still the insecure default. apps/api/.env still holds the example key. Generate one and paste it into the file — see step 3. For a quick local try, LINKR_DEBUG=true in the same file also lifts the block — never on a machine reachable from the network.

Port 3000 is busy. The dev script usually offers another port automatically, or close the process that holds it (lsof -i :3000 on macOS/Linux).

Blocked request. This host is not allowed. The development server does not recognise the domain name you are calling it by. Add that name to WEB_ALLOWED_HOSTS in apps/web/.env.local — see “Installing on a remote machine” above.

From a remote machine, the page does not open at all. The server listens on localhost only: add WEB_HOST=0.0.0.0. If the page opens but stays blank and the application’s files return 404 errors, BASE_PATH is the cause — set when it should not be, or the other way round.

From a remote machine, the page shows but nothing loads. API calls go to http://localhost:8000, which the browser resolves on your workstation rather than the remote machine. Set VITE_API_URL=/ in apps/web/.env.local.

The app shows a login page when you wanted client-only mode. That means VITE_API_URL is set, usually in apps/web/.env.local. Empty it, or run npm run dev:client.

The frontend cannot reach the backend. First check the API answers (curl localhost:8000/api/v1/health). If it does but the browser reports CORS errors, LINKR_CORS_ORIGINS does not contain the frontend’s exact address — the port matters.

Next steps

  • Build your first project: Your first project.
  • Understand what each mode enables: Deployment modes.
PreviousWith DockerNextClient-only

Product

  • Home
  • Demo

Resources

  • Documentation
  • Resources
  • Tools
  • Blog

Community

  • Framagit source code
  • Github source code

About

  • InterHop.org
  • Contact

2021–2026 InterHop — CC BY-NC-SA 4.0 (site) · GPLv3 (software)