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 --versionon 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.localGenerate 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, installapps/api[dev,postgres]instead ofapps/api[dev]. apps/web/.env.localswitches the app into server mode: it setsVITE_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:allstarts the backend and the frontend in the same terminal, every log line prefixed by its source (weborapi).
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.
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.
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.
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:clientThe 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 onlocalhostonly, meaning the inside of the machine: nothing gets out. Set0.0.0.0so 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 underchu-example.orgis therefore rejected withBlocked 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 instancehttps://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, if3000is 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.orgThe 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
| Command | Effect |
|---|---|
npm install | Install all monorepo dependencies. |
npm run dev:web | Start frontend in dev mode (port 3000). |
npm run dev:client | Start the frontend client-only, even if VITE_API_URL is set. |
npm run dev:api | Start the FastAPI backend (port 8000). |
npm run dev:all | Start both at once (needs npm install at the root). |
npm run data:fetch | Download the demo data into the seed folder. |
npm run build | Produce production build for all workspaces. |
cd apps/web && npm run preview | Serve 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.