Upgrade from 2.0 through 2.9
Back up an existing Lific instance, check client and hosting compatibility, upgrade to 2.9, and verify the result before resuming work.
This guide covers existing installations from Lific 2.0 through 2.9. Read the sections newer than your installed version before upgrading to 2.9. You do not need to install each intervening binary: the new binary applies pending migrations on startup. Migration paths from versions older than 2.0 are outside this guide.
If you use CLI device login, upgrade the CLI before a server older than 2.8. For multi-instance MCP, plan to upgrade all backends to identical tool definitions before restarting the proxy. Keep the pre-upgrade backup: rolling back a migrated database requires restoring the backup with a compatible binary.
Before upgrading
-
Record the selected configuration and database paths with
lific instance infoand your service status command. -
Make a complete backup while the service is stopped or quiesced:
lific dump --out ./lific-before-2.9.tar.gzThe archive includes a consistent database snapshot, attachments, and
manifest.json. Keep a separate copy of the selected configuration and service definition. Use a whole-instance dump for recovery, not a Markdown export or a project archive, which omits accounts and permissions. -
Record how clients connect: browser URL, API-key users, OAuth clients, and MCP configuration files. Keep one known-good operator key available for recovery.
-
If the instance is behind a reverse proxy, review
server.trusted_proxiestwice: before starting 2.2, which introduced the setting with a default that trusted loopback peers, and again before 2.7, where the default changes to trust no proxies at all. From 2.7, a deployment whose proxy connects from loopback (Tailscale serve or Funnel, or nginx on the same host) must list that proxy explicitly, or every client shares the proxy's rate-limit bucket. Add only isolated proxy networks you operate. -
Confirm that
lific.tomlparses before rolling out 2.6. From 2.6 a config file that exists but cannot be read or parsed stops startup instead of falling back to defaults, and unknown keys are rejected. Runlific doctor, which reports the configuration as one check and continues through the rest. -
From 2.9,
lific doctorinspects the database without migrating it. A report of pending migrations does not apply them. Normal server startup applies them, or you can explicitly requestlific doctor --repairafter taking the backup. Do not use--repairas a harmless preflight against your only copy.
Stop the background service before replacing the binary or restoring a backup. Keep the same lific.toml, database path, attachments directory, and public URL unless you are intentionally relocating the instance.
Changes to account for
v2.1
lific membermanages project membership and roles from the CLI.lific user set-passwordprovides an operator password reset and invalidates the user's sessions.--configis honored byinitand service installation, and configuration/database discovery follows standard OS directories.- Private instances may set
[auth] required = false; this is a shell-controlled operator setting and must remain local or firewalled.
v2.2
- Browser views use credentialed realtime invalidation. Sessions are revalidated, connections are capped per user, and reconnects trigger a resync.
server.trusted_proxiescontrols when forwarded client-IP headers are trusted for rate limiting.- MCP output is compact by default. Use the documented opt-in fields such as
include_closed,include_comments, orecho_treewhen an agent needs the full view. - MCP search supports literal mode, comments are searchable, and list/comment operations expose pagination hints.
- The web UI adds list sub-tabs, sidebar recents, touch page movement, PWA install metadata, and improved command-palette results.
- Auth-optional mode now reaches the browser through the instance auto-login flow; with no accounts, the signup screen still appears.
v2.2.1
- The three export MCP tools are now one
exporttool. It dispatches by identifier:PRO-42for an issue,PRO-DOC-3for a page, and barePROfor a project. - The MCP surface is 27 tools. Clients that assumed the older 29-tool count or the removed export tool names must refresh their discovery data.
v2.3
- The CLI can run data commands against a running server with
--backend http, using--urlorLIFIC_URLand--api-keyorLIFIC_API_KEY. The direct SQLite backend remains the default. - OAuth discovery works on an instance with no
server.public_url. An explicitpublic_urlis still authoritative.
v2.4
- One migration runs automatically on first launch. Project groups start empty for every user, so the sidebar renders as before until you create one.
v2.5
- No migrations. Identifiers render as Markdown links in MCP, CLI, and REST output;
server.public_urlpins the link base, and Lific otherwise derives it from an allowlisted request host.
v2.6
- Two migrations run automatically on first start. They add the per-tool identity plumbing behind connected agents, and there are no manual steps.
- A configuration file that exists but cannot be read or parsed is now a fatal startup error, and unknown keys are rejected. A missing file still starts on the built-in defaults. Check that
lific.tomlparses before the rollout;lific doctoris the tool for it. - A server with
web_auto_loginenabled refuses to start when[server] hostis not loopback. Version 2.7 also checksserver.public_url, as described below. - REST authentication failures are uniformly
403with the messageauthentication required. Endpoints for profile, password, sessions, API keys, connected tools, and comments previously answered400for this case. A client that reads400from those endpoints as "signed out" must key on403instead. - Remove
secure_cookiesfrom[auth]if your config sets it. It has never been read from the file, and unknown keys are now an error rather than being ignored. lific initpresents an auth-mode choice on a fresh install and creates the first administrator directly. An existing instance with users skips this entirely.- Connected AI tools have identities of their own. Audit entries for their writes are attributed to the tool rather than to the operator who connected it.
v2.7
Go to 2.7.1, not 2.7.0. In 2.7.0 every issue page in the web UI failed to render, on every instance. 2.7.1 fixes it and changes nothing else. The two releases are otherwise identical, so everything below applies to both.
The largest upgrade since 2.0. Three things need an operator decision before you roll it out: server.trusted_proxies stops defaulting to loopback, an older binary will refuse to open the database once 2.7 has touched it, and account recovery revokes much more than it used to. Take the pre-upgrade archive described above; on this release it is the only way back.
Account recovery changed shape. Read this part before rolling out if anything automated holds an API key.
-
API-key callers lose credential-minting permission.
POST /api/auth/keysandPOST /api/auth/botsnow require a browser session token created within the last 15 minutes and answer403 recent authentication requiredto anything else, API keys included. This is deliberate: a key that could mint another key outlived the revocation of the key it came from. A script that provisioned keys by presenting an existing key must move tolific key createon the server, which writes to the database directly.lific connectis unaffected for the same reason. -
A password change or sign-out-everywhere now revokes far more than sessions. For the account and every connected-tool bot it owns: sessions, API keys, OAuth access tokens, unexchanged authorization codes, and uncollected device approvals.
lific user set-passworddoes the same. Plan for reconnecting tools and reissuing any API key a script depends on after a reset. Unbound operator keys are not affected. -
A password change returns a replacement session in
token, and it is the only credential left that works. A REST client that changes a password on a user's behalf must store that token before its next request. The web UI does this for you. -
A running stdio MCP agent stops at its next tool call once its key is revoked.
LIFIC_TOKENis revalidated on every tool call rather than only at startup. Expect agents to needlific connectplus a client restart after any recovery. -
OAuth access tokens can no longer approve an authorization request or a device code, and the browser session that approves must have signed in within the last 15 minutes. Approve from a signed-in browser, signing in again first if prompted. A tool that scripted its own re-approval through a token it already held will stop working.
-
Every access-expanding admin endpoint requires a recent browser session.
POST /api/users,POST /api/users/{id}/promote,POST /api/users/{id}/reactivate,PATCH /api/instance/settings,POST /api/projects/{id}/members, role increases throughPATCH /api/projects/{id}/members/{user_id},PUT /api/projects/{id}when it setslead_user_id(which grants a lead membership), andPOST /api/projectswhen it names a lead other than the caller. Creating a project with no lead, or with yourself as lead, is unchanged, so automation that creates its own projects keeps working. Each answers403 recent authentication requiredto an API key or OAuth token, because each leaves access behind that a lockdown aimed at the calling credential cannot reach. Scripted user provisioning moves tolific user createandlific user promoteon the server. Reductions are untouched:demote,deactivate, role downgrades and member removal all still work from an API key, so containment stays fast. -
Instance settings are gated as a whole, not field by field. A patch that only changes
instance_nameis refused from an API key just like one that turns onweb_auto_login. Classifying each field as expanding or not is a rule that has to be re-derived every time a field is added, and one miss reopens the hole. -
An OAuth grant that is not bound to an identity can no longer be exchanged. Authorization codes and device approvals stored before 2.1 have no user attached; exchanging one produced an access token that named nobody and was therefore treated as the operator, and which no account recovery could revoke. Both exchange paths now return
invalid_grantfor these. Codes expire in ten minutes and device codes in fifteen, so in practice this only affects a client mid-flow across the upgrade: have it authorize again. Access tokens already issued are unchanged; revoke any unbound one you find withDELETE FROM oauth_tokens WHERE user_id IS NULLand reconnect the tool. Rolling back is now a restore, not a binary swap. -
Five migrations run automatically on first start: bot-identity uniqueness (which merges any duplicate agent identities into the oldest, keeping memberships, groups, and saved views), case-insensitive project identifiers, the account active flag, attachment dimensions and alt text, and the attachment search index. The
_migrationstable also gains a checksum column, backfilled from the migrations this binary carries. No manual steps. -
An older Lific will refuse to open a 2.7 database. Migrations only run forwards, so an older binary used to find the version already stamped, apply nothing, and serve a schema it was never compiled against. Startup now fails with an error naming the database's schema version and the highest the binary supports. To go back, restore the pre-upgrade archive.
-
A hand-edited migration file is now a startup failure. Applied migrations are checksummed, and a stored hash that no longer matches names the migration and both digests. If you have ever patched a migration in place, restore the original before upgrading and put the correction in a new migration.
-
[backup] retain = 0andinterval_minutes = 0no longer do what they said. Zero retention deleted every archive seconds after writing it, and a zero interval panicked the backup task on its first tick, silently ending backups while the server kept serving. Both now warn and fall back to the defaults, 24 archives every 60 minutes. Setenabled = falseto turn backups off.
Bounded responses. Every surface that could return an unbounded result is now paged or capped. A client that relied on getting everything in one call needs to page.
- Comment lists default to 50 and cap at 500.
GET /api/issues/{id}/commentskeeps itsorder=ascdefault, so an unqualified read is now the oldest 50; page withoffset, or passorder=desc. The MCPlist_commentstool andlific comment listdefault to the 50 newest instead, so a script that expectedlific comment listto print a whole thread oldest-first needs--order ascand its own offset loop.get_issuewithinclude_comments='all'returns at most the 500 most recent. - A comment body is capped at 256 KiB, measured after newline and tab normalization. Existing longer comments stay readable.
- Search is bounded and scoped. Page size defaults to 20 and caps at 500, offsets clamp at 100,000, a full-text query over 4 KiB or a literal query over 256 bytes returns 400, and a literal search stops at 10,000 matches. Results from projects the caller cannot see are gone, so counts may legitimately drop; filtering by an invisible project now returns empty rather than an error that revealed it exists.
- Exports are bounded. 10,000 files, 1,000 comments per issue, 100,000 per project, 50,000 metadata items, 8 MiB per file and 128 MiB total. Previously an export asked for at most 10,000 issues and then loaded every comment on each with no limit, so a large project's export was bounded only by available memory; one past the new ceilings now fails with an explicit error. Two run at a time; the rest get 429 with
Retry-After: 30, which an export client should honour. The HTTP download is dropped after 30 seconds idle or 30 minutes in total. - Websocket clients are capped at 16 sockets per user and 1,024 per instance, with 16 KiB messages, 4 KiB frames, 64 messages per 10 seconds, and a 5-second send timeout. Clients that only listen are better off: the server now sends protocol pings every 30 seconds, so they are no longer dropped after 120 seconds for skipping an application heartbeat.
Other behavior changes.
- OAuth access tokens are refused on every credential-management route, which covers API key create/list/revoke, connected-tool management, password change, session revocation, profile edits, user administration, and OAuth client and token management. A connected tool that called those endpoints gets a 403 and needs a browser session or a local CLI command. Ordinary reads and writes through MCP are unchanged.
- Editing or deleting a comment now requires access to its project, not just authorship, on both REST and MCP. Someone removed from a project loses the ability to mutate the comments they left in it.
- Project identifiers resolve case-insensitively across CLI, REST, and MCP, matching modules, folders, and usernames. Creating
abcwhenABCexists is now rejected, and a database that already holds a case collision (only reachable by raw SQL) has them renamed deterministically on upgrade. - Passwordless startup and enabling browser auto-login require a loopback bind and an absent, empty, loopback, or private-network
public_url. The guard used to look only at the bind host, which passed a loopback bind published through Tailscale Funnel or a same-host proxy. To recover an exposed instance, runlific instance set --auto-login falseand set[auth] required = truebefore restarting. - The CLI refuses to send an API key over plaintext HTTP to a remote host. What was a warning is an error. Use
https://or a loopback target.LIFIC_TOKENis now sent only to the origin named inLIFIC_URL, so a token plus a--urloverride, or a directory holding alific.tomlfor a different server, falls back to stored credentials for that host. - Uploads are validated by magic bytes rather than the declared content type, capped at 10 MiB by default, and rate-limited to 30 per user per 10 minutes. A client uploading a type outside the allowlist is refused.
- MCP tool errors are generic. Raw database text, which could carry table and column names and the database path, no longer reaches the client; it stays in the server log. A script matching on that text needs another signal.
v2.8
Upgrade CLI device-login clients before upgrading the server. Lific 2.8 requires a registered OAuth client for device authorization; a pre-2.8 CLI sends the old request and cannot log in to a 2.8 or newer server. A current CLI can still log in to an older server, so CLI-first is the compatible order.
OAuth clients and approvals. Automated clients using
POST /oauth/device_authorization must register at /oauth/register, then
send the returned client_id. Free-text client_name is no longer sufficient.
An omitted scope defaults to mcp; an explicitly different scope is refused.
lific login stores the registration in ~/.config/lific/clients.json, keyed
by server URL, and reuses it on later logins. This file contains no secrets.
Deleting it causes a new registration on the next login; do not delete it as
routine upgrade maintenance and consume registration slots unnecessarily.
Device codes issued before the server upgrade must be restarted. They have no
registered client attached and token exchange returns invalid_grant; codes
expire after 15 minutes. Approval now first shows the registered client and
requested access, then requires an explicit approval. Entering a device code
alone does not approve it. Authorization-code approval also requires an explicit
Approve or Deny choice, bound to the request displayed.
Both approval flows require a signed-in browser. Keep the recent-session rule from 2.7: sign out, sign back in and start a new connection if your approval session is too old. Existing account-recovery revocation rules still apply. PKCE clients must send valid S256 challenges and verifiers with the required 43 to 128 characters. Registered redirect URIs may not contain fragments.
Live sync and retained deletes. Issues, pages and comments now carry sync
sequence numbers. WebSocket clients can resume by cursor; sync_required
asks the client to backfill through the project's /changes endpoint. Fresh
clients bootstrap through /index. Existing clients may ignore the added
protocol fields, but custom clients adopting replay should persist and advance
the returned cursor rather than infer ordering from timestamps.
Deleting an issue, page or comment now retains a tombstone. The default
retention.trash_days is 30 days; the retention sweep permanently purges expired
rows. Maintainers can restore issues and pages through REST. There is no trash
UI in this release, and soft deletion does not replace a backup. A restore emits
ordinary create or update events, which older clients can receive.
Updates may include expected_seq to reject a stale write with HTTP 409 and the
current state. Clients omitting it retain last-writer-wins behavior. Adopting
sequence-based conflict checks is optional, not a new requirement on every
existing update.
Other client changes. SVG attachments download instead of rendering inline.
Module status writes must use backlog, planned, active, paused, done
or cancelled; previously stored free-text values are not a supported write
format. Attachment uploads and deletes can answer HTTP 503 during backup,
dump or restore, with Retry-After: 2; retry after that delay. Restore now
validates metadata, schema integrity and attachment bytes before installing an
archive, so a damaged archive that previously appeared usable may be refused.
v2.9
Four migrations run when upgrading a 2.8 database:
| Migration | Change |
|---|---|
| 049 | Repository bindings and their identity aliases. |
| 050 | Project archive attribution and provenance. |
| 051 | Per-project publication, off by default. |
| 052 | Personal sidebar project ordering. |
Existing projects remain private. Sidebar ordering uses the existing ranks until a user reorders projects; that user's changes no longer rearrange other users' sidebars. To roll back after migration, stop the service and restore the pre-upgrade whole-instance backup with a compatible binary. Do not point a 2.8 binary at the migrated database or edit migration records to make it start.
Initialization is deliberate. Ordinary local commands, including bare
lific mcp and lific start, refuse a missing database and report its expected
path. An automation job must initialize with lific init, restore a backup,
or use the guarded first-boot path below. HTTP-backed commands and remote MCP
proxies still need no local database. If a previously working service suddenly
reports a missing database, check its path and mounted volume before creating
anything; a new empty instance is not recovery of the old one.
lific init --db records the actual absolute database path in its configuration
and reports conflicts with an already configured path. It can update an existing
writable config file even when the parent directory is read-only, as with a
container-mounted config. This does not make an unwritable file writable.
The supplied container image starts with start --init-if-missing. On an empty
volume, this requires a deliberately selected database location, an existing
writable parent directory and both environment variables:
LIFIC_INIT_ADMIN_NAMELIFIC_INIT_ADMIN_PASSWORD
Provide the password through your deployment's secret mechanism, not a committed configuration file or image layer. Bootstrap creates a password-protected administrator with signup and browser auto-login off, and does not print an operator key. Interrupted bootstrap can resume. An initialized instance that already has its admin ignores these variables; changing them does not reset that account's password. Remove bootstrap credentials from the deployment environment after confirming first boot. See self-hosting for the current container setup.
Diagnostics and credentials. lific doctor is read-only with respect to
database migration unless you pass --repair. Configuration errors remain
visible instead of selecting a default database. The CLI also refuses malformed
credential storage and unsafe filesystem paths rather than treating them as
an empty store or overwriting them. Preserve the failing file for diagnosis,
correct the reported format or filesystem problem, then retry login. Do not
silently replace it with an empty credential store in automation.
Account recovery through the CLI now stops protected WebSocket delivery without waiting for the idle revalidation timer. Credentials are revalidated before protected live deliveries, cached activity reads and replay frames. Reconnect affected agents and API clients after a reset using the 2.7 recovery procedure. Enabling browser auto-login through the CLI now applies the exposure checks used by the web API; disabling it remains available for recovery.
Comment clients must follow continuation metadata. Interactive comment responses have a 2 MiB byte budget as well as a row limit. A short page no longer means that a thread ended. REST exposes:
| Header | Meaning |
|---|---|
x-comment-has-more | Whether another page remains. |
x-comment-returned | The number of comments returned in this response. |
x-comment-next-offset | The next offset for offset-based pagination. |
x-comment-next-created-at, x-comment-next-id | The continuation pair for cursor-based pagination. |
Use the continuation for the paging mode you requested, rather than adding the
requested limit to your offset. For cursor paging, pass the returned pair as
before_created_at and before_id. A newest-first offset response can also
provide that pair so you can switch to cursor paging. MCP counts the encoded
response, including formatting, and reports continuation instructions too. An
individually oversized comment fails explicitly rather than disappearing from
the thread. Handle that error and its instructions; do not mark the thread
complete. Comment writes allow at most 256 distinct mention tokens and 256
distinct attachment IDs. Legacy oversized bodies can be edited without growing.
Imported comments can report user_id: -1. This means there is no destination
author account, not a user ID to resolve. Their displayed authors are inert
names marked (imported). A destination account with a matching username does
not gain authorship or permission over those comments.
Repository context and multi-instance routing. Repository bindings supply defaults only for the documented project-scoped MCP tools. Search remains cross-project when unscoped, workspace page creation remains available, and destructive tools do not inherit an implicit project. Explicit projects win. Restart stdio sessions after setting or changing a binding.
Multi-instance MCP requires complete tool definitions
to match across every backend at startup, including descriptions, annotations
and schemas. Equal version numbers or compatible input schemas alone are not
enough. Upgrade the backends together before restarting the proxy. With several
backends, writes require an explicit instance; only allowlisted reads may use
the configured default. Calls never fall back to another tracker after failure.
Resources and prompts are outside this routing mode.
Archives and publication are opt-in. A project archive creates a new private destination; it does not merge with an existing project, delete the source or transfer memberships and credentials. It includes historical text removed from current descriptions, so treat the file as private. If an import loses its connection, its outcome is unknown. Check the destination before retrying, since the server may already have committed it.
Publishing a project exposes its existing current issues, pages, comments and linked files to anyone. Review them before publication. Plans, history and trash stay outside the public read surface. Unpublishing stops subsequent requests but cannot recall downloaded material. Require instance authentication and disable browser auto-login before exposing the server; publication does not secure other routes for you.
Upgrade and verify
-
Upgrade CLI clients first if the server is older than 2.8 and those clients use
lific login. Install the v2.9 binary and restart the existing service with the same configuration and database. Confirmlific --versionusing the executable your service actually runs, not an unrelated binary on PATH. -
Run
lific doctorwith the operator credential. Resolve configuration, database, backup, server, OAuth, and MCP failures before opening the instance to users. -
Check
lific instance infoand confirm the expected database, public URL, signup policy, andauthz_enforcedstate. -
Review membership and operator keys:
lific key list lific member list --project <PROJECT>Unbound API keys are operator-trusted/admin-equivalent because they can only be minted with shell access. Audit or rotate any key that is no longer needed. Use user-owned bot keys when an agent should inherit project membership.
-
Test the browser login and one representative project workflow. Confirm issue, page, plan, comment, attachment, and saved-view data before testing writes.
-
Test one API and one MCP client. For MCP, refresh tool discovery and replace calls to
export_issue,export_page, orexport_projectwithexport. With multi-instance MCP, confirm every backend passed discovery and inspectlist_instancesbefore trying writes. Test comment pagination through its continuation metadata. Restart device login flows begun before the upgrade. -
If the instance uses a proxy, confirm the server logs the expected client IP and that rate limits do not trust arbitrary forwarded headers.
-
Keep the pre-upgrade archive until the browser, API, MCP, backups, and service restart have all been verified.
-
Confirm existing projects remain private. If you deliberately publish one, test its anonymous URL while signed out and verify ordinary private routes still require authentication. If you import an archive, review its report and linked files before switching writers or enabling publication.
Authorization rollout
An existing 2.0 instance keeps its persisted authorization setting during the upgrade. Inspect it before changing anything. With enforcement on, viewers can read and comment, maintainers can mutate project content and structure, and leads manage settings, membership, and deletion. Cross-project relations and plan-step links require the appropriate role in every project involved.
To change the setting deliberately:
lific instance set --authz-enforced trueBefore enabling it, ensure every human and bot that needs access has a project membership, and keep an operator-trusted key available for recovery. To roll back the setting on a private instance, use --authz-enforced false and then investigate the missing membership or role rather than distributing a broader key.
For a private local deployment, [auth] required = false is a separate setting from project authorization. It grants credential-less operator access and is unsafe anywhere others can reach it. Startup requires a loopback bind and an absent, empty, loopback, or private-network public_url. Treat both settings as operator changes that require shell access and a verification step.
If the upgrade fails
Stop the service, preserve its logs and the failed database state, and do not repeatedly rerun migrations against the only copy. Restore the pre-upgrade archive to a separate data directory or use lific restore <ARCHIVE> --force only after confirming the archive and intended target. Re-run lific doctor, then compare the restored instance's configuration and client credentials before switching traffic back.