Lific

Self-hosting

Run Lific for a team: sign-in mode, the server process, reverse proxies and TLS, accounts, backups, and day-to-day operation.

A Lific instance is one process and one SQLite database. Attachments live in an attachments directory beside the database, and automatic backups in a backups directory beside it as well. Nothing else holds state, so moving an instance means moving those files and pointing a new process at them.

This page covers running that process for other people. For a tracker only you use, Quickstart is enough. Every setting named here is listed in Configuration.

Choose how people sign in

lific init asks this once, on a fresh instance, and the answer decides most of what follows.

ModeWhat init writesUse it for
Passwords[auth] required = true. Your bind host is left as it is. You pick an administrator password.Any instance someone else can reach.
Login-free[auth] required = false, [server] host = "127.0.0.1", browser auto-login on, and an administrator with no password.One person on one machine.

For a team, choose passwords. Login-free mode treats every request that carries no credential as the administrator, so anyone who can reach the port runs the instance. Lific refuses to start in login-free mode when the instance looks reachable from elsewhere; see Authentication for the exact rule.

Pass the choice as flags to skip the prompts: --auth-mode passwords --name "Ada Lovelace", plus --password or a password on standard input.

Run the server

lific start --config /etc/lific/lific.toml

start serves the web UI, the REST API, the /mcp endpoint and the OAuth routes in the foreground until it receives a shutdown signal. --host and --port override the configured values for one run.

start does not create a database. Set the instance up first with lific init, using the configuration the server will run with. Containers are the one exception, with their own bootstrap.

The process needs read access to the configuration file and write access to the directory holding the database. That directory also receives attachments/, archived-projects/ and, by default, backups/.

As a user service

On a machine where Lific runs under your own login, lific init installs a service and starts it unless you pass --no-service. lific service manages it afterwards:

lific service status
lific service restart
lific service stop
lific service uninstall

On Linux this is a systemd user unit at ~/.config/systemd/user/lific.service, restarted on failure. Lific also runs loginctl enable-linger so the service keeps running after you log out; if that fails, init says so, and the server stops at logout until you enable lingering yourself. On macOS it is a LaunchAgent at ~/Library/LaunchAgents/dev.lific.plist. On Windows it is a Lific value under your account's Run registry key that starts a supervisor Lific runs itself; see Windows.

The service runs the lific binary you installed it with, with the configuration file you selected, and its working directory is that file's directory. A relative database.path resolves against the configuration file's directory in any case, never against the directory you ran a command from.

As a system service

On a shared server, run Lific as its own system user, with the configuration outside anyone's home directory. Create the user, then give it a configuration directory and a data directory it owns:

sudo install -d -o lific -g lific -m 700 /etc/lific /var/lib/lific
sudo -u lific lific --config /etc/lific/lific.toml --db /var/lib/lific/lific.db \
  init --no-service --auth-mode passwords --name "Ada Lovelace"

Run init as the lific user so the files it creates belong to that user. Because /etc/lific/lific.toml does not exist yet, init writes it with database.path = "/var/lib/lific/lific.db", creates the database and your administrator, and asks for the password. --no-service stops it from installing a user service for lific. Edit the configuration afterwards for the settings in the rest of this page.

Then write a unit such as this to lific.service in the system unit directory, reload systemd, and enable and start it:

[Unit]
Description=Lific issue tracker
After=network.target

[Service]
User=lific
Group=lific
ExecStart=/usr/local/bin/lific start --config /etc/lific/lific.toml
WorkingDirectory=/var/lib/lific
UMask=0077
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

Containers

The repository ships a Dockerfile. It exists mainly so MCP directories can build and check Lific, and it works as a deployment too. No image is published, so build it yourself:

docker build -t lific .
docker run --name lific -p 127.0.0.1:3456:3456 \
  -v lific-data:/data \
  -e LIFIC_INIT_ADMIN_NAME \
  -e LIFIC_INIT_ADMIN_PASSWORD \
  lific

The two -e flags pass through variables you have already set in your shell, so the password stays out of the command line. The image runs lific --db /data/lific.db start --init-if-missing as the non-root user 65532 and listens on port 3456. The database, attachments and backups all live under /data, so keep that volume across container replacements. A new named volume inherits the writable /data the image prepares. For a bind mount, create the host directory first and give user 65532 write access to it.

--init-if-missing is the only way to create a database other than lific init, and it refuses rather than guessing:

  • It refuses when nothing chose the database path. An explicit --config or --db, or a configuration file that was found, counts. The built-in relative default does not, so the flag never leaves a stray lific.db in whatever directory the process started in.
  • It refuses when the database's directory does not exist, rather than creating it. A missing directory is what a volume that failed to mount looks like, and writing into the container's own filesystem would lose everything on the next restart.
  • It refuses when that directory is not writable by the process.
  • It refuses to create a database without an administrator. Set both LIFIC_INIT_ADMIN_NAME (the display name) and LIFIC_INIT_ADMIN_PASSWORD. Setting only one is an error.

An instance that booted with no users would have open signup, would make its first visitor the administrator, and would print an operator API key into the container log. So first boot creates the administrator, closes signup whatever [auth] allow_signup says, leaves browser auto-login off and keeps authentication required. It prints no API key. Create keys afterwards from inside the container, for example docker exec lific lific --db /data/lific.db key create --name ci --user ada-lovelace, or connect tools from the web UI.

Once the database exists, the two variables are ignored and you can drop them. They are not a way to reset a password. The one exception is a database that was created but never got its administrator, which is what a crash during first boot leaves behind. The next start with both variables set finishes the setup; without them, it refuses to serve an instance nobody can administer.

--init-if-missing works outside the image under the same rules.

Reach it over HTTPS

Lific speaks plain HTTP on [server] host and [server] port, 0.0.0.0:3456 by default, which is every network interface. It does not terminate TLS. Put a reverse proxy in front of it (nginx, Caddy, Tailscale Serve or Funnel, a CDN), bind Lific to loopback so only the proxy can reach it, and tell Lific the address people actually use:

[server]
host = "127.0.0.1"
port = 3456
public_url = "https://lific.example.com"
cors_origins = ["https://lific.example.com"]
trusted_proxies = ["127.0.0.1"]

public_url does several jobs:

  • It is the OAuth issuer that MCP clients discover, and the base of the MCP URL lific connect writes.
  • Its host name is added to the hosts the /mcp endpoint accepts. Without it, /mcp accepts only requests whose Host header is localhost, 127.0.0.1 or ::1, and refuses a proxy that forwards your public host name, or a client that reaches the server by its LAN address.
  • It is the base of issue links in CLI and MCP output.
  • Its scheme decides whether the session cookie is marked Secure. An https:// URL, or none, keeps Secure on. An explicit http:// URL turns it off, because browsers drop Secure cookies over plain HTTP and the OAuth approval page would stop working.

cors_origins = [] allows every browser origin. List your origins once a browser page on another site needs to call the API.

The proxy must pass /api/events/ws through as a WebSocket, including the Upgrade and Connection headers, with an idle timeout long enough to hold the connection open. If pages work but changes made in another tab or by an agent never appear until you reload, check this first.

Client addresses behind a proxy

Lific rate-limits sign-in, signup, session refresh, password changes, OAuth client registration, device authorization and public project reads by client address. Behind a proxy, every request arrives from the proxy's address, so Lific has to be told which peers may speak for someone else. server.trusted_proxies lists those IP addresses or CIDR ranges. The default is empty, which means every client of an undeclared proxy shares one rate-limit bucket. An invalid entry stops the server at startup.

Forwarding headers from any other peer are ignored. For a trusted peer, Lific reads all X-Forwarded-For values as one chain, walks it right to left, skips the addresses in trusted_proxies, and uses the first address that is not a trusted proxy. X-Real-IP is read only when there is no X-Forwarded-For at all, and a malformed or entirely trusted chain falls back to the proxy's own address. List every proxy layer you run, and nothing else. If your proxy sets X-Real-IP rather than appending to X-Forwarded-For, make sure it overwrites any value the client sent.

If you cannot control the whole chain, for example because a CDN in front of your proxy appends its own entries, the proxy can send the client address directly instead. Set a long random LIFIC_TRUSTED_PROXY_SECRET in the server's environment, and have the proxy send two headers on every request:

HeaderValue
X-Lific-Client-IPThe client's address, as a bare IP.
X-Lific-Proxy-SecretThe same value as LIFIC_TRUSTED_PROXY_SECRET.

Lific honors the pair only from a peer in trusted_proxies. When a trusted peer sends either header and the pair does not check out (the secret is wrong, one header is missing, or the server has no secret set), the rate-limited routes above answer 503 instead of falling back to the proxy's address. Other routes ignore the headers. The proxy must remove any copies of both headers the client sent. Because the server accepts one secret at a time, rotating it means changing the server and the proxy back to back, and sign-in fails with 503 in between.

Authentication

Keep [auth] required = true on any instance other people can reach. Every REST and MCP request then carries Authorization: Bearer <credential>, where the credential is an API key, an OAuth access token, or the session token the web UI holds. Public projects are the only anonymous exception: a project lead or administrator can publish one project's current issues, pages, comments and linked files for anyone to read.

Create API keys on the machine running Lific:

lific key create --name ci --user ada-lovelace
lific key create --name nightly-report --user ada-lovelace --expires 2027-01-01

The key is printed once. A key assigned to a user acts as that user, with that user's project roles. A key created without --user is unbound: it acts as the first administrator and ignores project membership entirely, so review those with lific key list. Connect agents creates one key per AI tool for you, and the OAuth routes let MCP clients sign in without a key.

lific start refuses to boot, rather than warn, in two cases:

  • [auth] required = false while the instance is reachable from beyond this machine.
  • Browser auto-login is turned on in the instance settings under the same condition, because it hands every visitor an administrator session.

"Reachable" means server.host is not a loopback address, or server.public_url is set to something that is not loopback or a private network. Private means a private, link-local or unique-local IP address, or a name ending in .localhost, .local, .internal, .home.arpa or .lan. A public DNS name counts as public even when it resolves to a private address, and that includes a Tailscale name such as host.tailnet.ts.net. The check reads only your configuration. A proxy you have not told Lific about is invisible to it, so passing the check does not prove the instance is private.

If server.mcp_path_token is set, /mcp/<token> answers without any other credential; the path itself is the secret. It exists for MCP clients whose OAuth support is broken. Use a long random value and serve it only over HTTPS.

Accounts and access

Signup is open on an instance created with lific init: anyone who can reach the server can make an ordinary account. [auth] allow_signup only sets the starting value the first time Lific opens the database. After that the stored instance setting wins, so change it in the web UI (Settings, then Instance) or with the CLI, not in the TOML file. A container bootstrapped with --init-if-missing starts with signup closed.

lific instance set --signups false
lific instance set --signup-domains "example.com"
lific user create --username grace --email grace@example.com --admin
lific user promote --username grace
lific user demote --username grace

--signup-domains limits self-registration to a comma-separated list of email domains, and an empty string allows any. user create prompts for the password when you leave out --password. --admin and user promote make someone an instance administrator, who can see and change everything; user demote takes it away. The same actions are in the web UI's Instance tab.

Project membership is a separate switch, the Instance tab's Project permissions or lific instance set --authz-enforced true. With it on, every REST and MCP call checks the caller's role in the project, reads included, and a new account sees nothing until someone adds it to a project, with lific member add --project APP --user grace --role maintainer or the project's Members settings. Administrators and unbound API keys bypass the check. It is on for an instance that had no users when its settings were first written, which includes instances created with lific init. An instance upgraded from an older release keeps it off until you turn it on; Upgrading explains how to roll it out. Projects describes the three roles.

Backups

Automatic backups are on by default:

[backup]
enabled = true
dir = "backups"
interval_minutes = 60
retain = 24

A relative dir resolves against the database's directory, and an absolute one is used as written. A new backup is written once the newest one in dir is interval_minutes old, and the newest retain are kept. The server checks five seconds after startup and then on schedule. So does lific mcp over stdio, and local data commands check after they finish, so an instance used without a server is backed up too. The schedule lives in the backup directory, so the server, any number of stdio sessions and the CLI share it rather than each writing their own archives. Restarting the server does not force a fresh backup; run lific dump when you want one now. The defaults keep a day of hourly backups. interval_minutes = 0 or retain = 0 is treated as a mistake: Lific logs a warning and uses the default. To turn backups off, set enabled = false.

Each backup is one archive named <database name>_YYYYMMDD_HHMMSS.tar.gz, such as lific_20260101_120000.tar.gz, readable only by its owner on Linux and macOS. It holds:

EntryContents
lific.dbA consistent copy of the database, taken with SQLite's online backup while the server keeps serving.
attachments/Every attachment file, named by content hash.
archived-projects/One file per archived project.
manifest.jsonThe Lific version, schema version, time, database size, and attachment and archived-project counts and sizes.

The archive covers everything in the database: every project, account, API key, setting and audit record. It does not include lific.toml, so keep a copy of that yourself. An interrupted backup leaves staging files behind, which the next cycle removes.

[backup] audit_retention_days is unrelated to the archives. It is unset by default, which keeps audit history forever; set it to delete audit records older than that many days at the end of each backup cycle.

Manual backup and restore

lific dump writes the same archive on demand, and is just as safe to run while the server is up:

lific dump
lific dump --out /var/backups/lific
lific dump --out /var/backups/lific/before-upgrade.tar.gz

--out takes a file name or an existing directory. Without it, the archive goes in the current directory. External backup tools can copy the automatic backups directory, or run lific dump first and copy its archive.

Stop the server before you restore. lific restore writes to the database path your configuration names, or to --db, and restoring underneath a running server corrupts it. Lific warns when a live write-ahead log next to the database suggests the server is still running.

lific restore /var/backups/lific/lific_20260101_120000.tar.gz
lific restore /var/backups/lific/lific_20260101_120000.tar.gz --force

Restore checks the archive before it touches anything. The manifest and database must be present, attachment entries must be plain content hashes that match their contents, the database must pass an integrity check, and its schema must not be newer than the running binary. The files are unpacked into a staging directory and moved into place, so a failed restore leaves your current data as it was.

Without --force, restore refuses to replace an existing database. With --force, the current database is moved aside to lific.db.pre-restore-<timestamp> and kept. The current attachments and archived-projects directories are replaced by the archive's and are not kept, so take a lific dump first if you might want it back. Start the server afterwards; if the archive came from an older release, its migrations run on startup.

Restore also caps the unpacked size, so a hostile archive cannot fill the disk before validation rejects it: 512 MiB of database, 64 MiB per attachment, 512 MiB per archived project, 1 GiB in total and 10,000 entries. A large instance can produce archives beyond that. --allow-large accepts them; use it only for an archive you made yourself. Every other check still runs.

To move one project to another instance, use a project archive instead. It carries the project's content and history but not accounts, credentials or membership, and it is no substitute for a full backup when an upgrade goes wrong. Read Upgrading before you replace a running binary.

The trash is not a backup

Deleting an issue, page or comment moves it to the trash. It disappears from lists, search, boards and public projects at once, a maintainer can restore it for 30 days, and then it is removed for good. Change the window with [retention] trash_days, or set it to 0 to keep deleted items until you remove them yourself.

The trash undoes a mistaken delete. Projects, plans, modules, labels and folders are deleted immediately and never go to the trash, and the trash does nothing for a lost disk, a failed upgrade or a damaged database. Backups cover those.

Operate it

GET /api/health answers 200 with the body ok and needs no credential. Point a container health check, load balancer or uptime monitor at it.

Logs go to standard output: the journal under systemd (journalctl --user -u lific for the user service), the container log under Docker, and lific.log in the configuration's directory for the macOS LaunchAgent. [log] level sets the level for Lific's own messages: trace, debug, info, warn or error. Messages from the libraries Lific is built on are off. The RUST_LOG environment variable replaces the whole filter, in the tracing filter syntax: RUST_LOG=lific=debug is the same as level = "debug", and RUST_LOG=info also shows every library's messages at info.

Every response carries these headers. A route that sets its own value keeps it; attachment downloads, for example, send a stricter sandboxing Content-Security-Policy.

HeaderValue
X-Frame-OptionsDENY
Content-Security-Policyframe-ancestors 'none'; base-uri 'self'; object-src 'none'
X-Content-Type-Optionsnosniff
Referrer-Policyno-referrer
Cross-Origin-Resource-Policysame-origin

Browsers therefore refuse to show Lific inside a frame on any page, including your own sites. Do not strip these headers at the proxy. To show Lific's data inside another application, use the REST API.

lific doctor checks the configuration, the database, the running server, OAuth discovery and a real MCP round trip, and exits with a nonzero status when something is wrong. Run it after any configuration change.

On this page