In short
An instance is configured through environment variables, all prefixed LINKR_. Three really matter: the secret key, the data directory and the allowed origin. The rest tune code execution, package repositories and how long the access log is kept. Settings editable inside the application are a separate matter, covered at the end.
The three that matter
Everything else has a sensible default. These do not.
LINKR_SECRET_KEY=<a long random string> # required
LINKR_DATA_DIR=/var/lib/linkr # this is what you back up
LINKR_CORS_ORIGINS=https://linkr.example.org # the real public address
LINKR_DEBUG=false
LINKR_DEBUG must stay false in production
This is not merely a convenience setting: debug mode is the only thing that allows starting with the example secret key or with a wildcard origin. It also changes the log format, from structured JSON to a human-readable console rendering.
The two keys
Linkr holds two, and mixing them up is the costliest mistake on this page: one only signs logins, the other seals stored secrets. Neither is derived from the other.
LINKR_SECRET_KEY
Signs login tokens
- Where it lives
- An environment variable, required: without it the server refuses to start.
- If it changes
- Everyone is logged out and signs in again. Nothing else is lost.
- If it is lost
- Same effect: generate a new one.
Encryption key
Seals stored secrets
- What it seals
- Remembered database passwords, IDE connection secrets, git tokens.
- Where it lives
- LINKR_ENCRYPTION_KEY if set; otherwise secret.key, in the data directory.
- If it is lost
- Those secrets become unreadable, with no error: everyone has to re-enter them.
The encryption key seals three things: the database passwords each user chooses to remember, the IDE connection secrets and the git access tokens. Each secret is sealed together with what it protects — the user, the database and its address —, so a secret copied into another row no longer opens. Account passwords are not encrypted but hashed: neither key concerns them.
Losing the encryption key breaks nothing loudly — and that is the trap
Decryption fails silently rather than with an error. The instance starts, the interface works, but every remembered database password, git token and connection secret becomes unreadable and has to be re-entered by hand. That is what happens when the application database is restored without the secret.key file.
Keeping the encryption key outside the folder
By default there is nothing to do: secret.key is created on first use, readable by the server alone, and a backup of the data directory takes it along with the rest. If your policy wants keys in a secrets vault rather than next to the data, supply it yourself:
# 32 random bytes, "url-safe" base64
LINKR_ENCRYPTION_KEY=$(python3 -c "import secrets, base64; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())")
For an instance already running, reuse the contents of secret.key rather than a fresh key: a fresh key would make the existing secrets unreadable.
Changing the encryption key
Put the new key in LINKR_ENCRYPTION_KEY and the old one in LINKR_ENCRYPTION_OLD_KEYS — several old keys are separated by commas. Each secret records which key sealed it: old ones stay readable, new ones are sealed with the new key.
Do not remove an old key from the list
Only database passwords are re-sealed under the new key, the next time they are used. Git tokens and IDE connection secrets stay sealed with the old one: removing it would make them unreadable.
The data directory
One variable names the place where everything is stored. The folder is created on first start.
Never two instances on the same folder
The disk is authoritative, with no mapping table: two instances sharing a data directory would tread on each other.
An automatic sweep runs at startup and every six hours: abandoned uploads, files nothing references any more, folders of deleted projects.
The application database
By default, a SQLite file stored in the data directory. For sustained multi-user use, PostgreSQL is recommended:
LINKR_DATABASE_URL=postgresql+asyncpg://linkr:password@localhost:5432/linkr
This choice is made at install time, not after
Changing engine on a running instance does not move the data: the instance would start from an empty database. That is why the interface does not offer switching to PostgreSQL — see below.
Code execution
This is the most sensitive part, and the most adjustable.
LINKR_ENABLE_CODE_EXECUTION=false # turns execution off across the instance
This switch overrides permissions
When it is false, nobody runs code, whatever their rights. The IDE, terminals, package environments and the server folder picker are all refused with an explicit message. Pointing a database at a path still works: attaching read-only data is not running code.
The limits
| Variable | Default | What it bounds |
|---|---|---|
| LINKR_EXECUTION_TIMEOUT_SECONDS | 120 | One interactive run. The clock restarts on each line of output: a chatty script is not cut off. |
| LINKR_JOB_TIMEOUT_SECONDS | 1800 | One background job. Unlike the previous one, this is a hard limit. |
| LINKR_MAX_KERNELS_PER_USER | 5 | Concurrent processes per user — code sessions and terminals together. |
| LINKR_SESSION_TIMEOUT_MINUTES | 60 | Idle time before a code session closes. The user’s login is untouched. |
| LINKR_MAX_BUILD_CONCURRENCY | 2 | Simultaneous long-running server work: environment builds and background jobs share this budget. |
| LINKR_BUILD_TIMEOUT_SECONDS | 1800 | One package-environment build. Without it, a hung build would hold its place in the previous budget forever. |
| LINKR_WIDGET_POOL_SIZE | 2 | R or Python processes kept warm, per language and per project, so code widgets render without waiting for an interpreter to start. |
| LINKR_WIDGET_MAX_CONCURRENCY | 8 | Code widgets running at once: a busy dashboard does not start one process per widget all at the same time. |
| LINKR_MAX_UPLOAD_MB | 2048 | The size of one upload. The shipped proxy caps lower — see Production install. |
Package repositories
Useful on a closed network, or to enforce an institutional mirror:
LINKR_PIP_INDEX_URL=https://pypi.internal.example/simple
LINKR_R_REPOS=https://cran.internal.example
The default R repository serves binaries
It points at a mirror distributing packages already compiled for Linux. A plain CRAN mirror would make the server compile each package, which lengthens environment builds considerably.
Other variables
| Variable | Default | What it sets |
|---|---|---|
| LINKR_FS_BROWSE_ROOTS | empty | The folders the server file picker may browse — see Files on the server. |
| LINKR_AUDIT_RETENTION_DAYS | 365 | How long the access log, described below, is kept. |
| LINKR_PASSWORD_MIN_LENGTH | 12 | The shortest password a local account may set (at least 8). Also required: different from the username, at most 72 bytes, and not on the list of the 100,000 most common passwords. |
| LINKR_POOL_TTL_SECONDS | 300 | How long a connection to a database stays open between two queries before it is closed for inactivity. |
| LINKR_KERNEL_TOKEN_EXPIRE_MINUTES | 720 | The lifetime of the token the R and Python libraries receive in a code session. Short on purpose: any code run in the project can read it. |
| LINKR_AUTH_PROVIDER | local | The authentication mode. local is the only value implemented — see Authentication and permissions. |
What about AI models?
Linkr calls no model itself. Agents run in an external client — LibreChat, OpenCode… — that holds its own model configuration, and act on the instance through the MCP server, with a user’s API key and permissions. Choosing a local or a remote model, and so deciding whether data may leave the institution, happens in that client. See MCP server.
What is set inside the application
Not everything goes through variables. The Settings page carries six tabs, each protected by a permission — the tab stays visible, but its contents are replaced by a notice if you lack yours.
Settings
Manage users and platform configuration.
app-database:readApplication database
Shows, read-only, the engine and location of the database the server actually uses, and lets you query it.
Organizations, users and roles are covered in Authentication and permissions, the catalogs — managed from the Catalog page itself — in Community catalog.
The General tab is read-only
It shows the database the instance actually uses: the absolute path of the linkr.db file in the data folder, or the host and name of the database LINKR_DATABASE_URL points at (never its password). To change it, edit LINKR_DATA_DIR or LINKR_DATABASE_URL and restart the server. Changing engine mid-life does not bring the data along.
Each workspace also has its own settings — members, badges, default environments — described in Workspace settings.
Logs and monitoring
In production, logs are emitted as structured JSON on standard output: collect them with your platform’s log collector. In debug mode they switch to a human-readable console rendering.
The access log
Every request that reads data, changes something or is refused leaves one line: who, when, from where, by which path — the interface, an API key, a code session, a background job —, on which database, with which SQL or code, and how many rows came back. Never the data itself. The SQL and code can still name a patient, so reading this log takes the audit-log:read permission.
A failed line also carries its reason: why a request was refused, or the database’s message when a connection fails (password hidden). Finally, every request gets a reference. When the application answers with a deliberately vague message, “Connection failed” or “Internal server error”, it adds that reference (“ref. 3f9a1c07be42”). It can be found in the tab’s Reference column and on every server-log line the same request produced, with the full detail, including an internal error’s stack trace.
It is written in two places at once:
- In the data directory, under
audit/: one file per day, compacted into one file per month. Months older thanLINKR_AUDIT_RETENTION_DAYSare deleted. - On standard output, with the other logs: that is the copy your platform’s log collector keeps.
An edit shows
Each line carries a fingerprint that depends on the previous one: editing or removing a line breaks the chain from there on. The tab’s Check integrity button verifies it. This does not protect against someone holding administrator rights on the server who rewrites everything — the copy on standard output, out of their reach, is what covers that.
What does not exist
There is no email sending — so no password reset by mail and no invitations: an administrator sets passwords by hand —, no log rotation to configure beyond the retention above, and no metrics exposed. To watch the instance you have only the /api/v1/health address, which answers if the server is alive.
Going further
- Production install — where to put these variables.
- Backup and restore — what the data directory implies.
- Files on the server — bounding what the picker reaches.
- Authentication and permissions — the rights that open these screens.