When something goes wrong
Find your symptom and follow its fix. Most fixes need the server stopped; stop it first so a repair cannot race live traffic.
- Unexpected sign-ins, a leaked credential, or someone to cut off now: Suspected compromise or a leaver
- The only Owner can't sign in: The sole Owner has lost their password
- Membership changes fail:
nextOwnershipTransferRevision - Startup stops on the audit outbox: Malformed or corrupted audit outbox record
CorruptAccountCommandStateErrororreconciliation_required: Account command needs reconciliation- Deleting a company fails:
TenantErasureIntegrityError - A company shows no Owner: A company has no Owner
SQLITE_FULLorbackup FAILED: Disk-full or a failed snapshot
Recovery commands (pnpm --filter capacitylens-server ...) need a git checkout with the full workspace installed. On Docker or a managed host, read Run recovery commands on Docker or a managed host first. The Owner password reset is the exception: the release archive carries its own tool.
Run recovery commands on Docker or a managed host
The packaged api image and a managed-host release omit pnpm, tsx and development dependencies, so docker compose exec api ... fails.
Docker:
Stop the stack:
docker compose stop.From a checkout of the release currently running (matching tag or commit), start a throwaway Node container with the checkout and both named volumes mounted:
bashdocker run --rm -it \ -v capacitylens_capacitylens-db:/data \ -v capacitylens_capacitylens-backups:/backups \ -v "$PWD":/workspace -w /workspace \ node:24-bookworm-slim bashAdjust the volume names to your Compose project prefix (Upgrades shows how to find it).
Before investigating, preserve
/data/capacitylens.db-wal,/data/capacitylens.db-shm,/data/capacitylens-audit.jsonland its.1rotation alongside the database. Install dependencies once withcorepack enable && pnpm install --frozen-lockfile, then run the documented command against/data/capacitylens.db, for examplepnpm --filter capacitylens-server recover:audit-outbox -- inspect /data/capacitylens.db. Snapshots are under/backups(named-volume restore procedure). Forsqlite3, runapt-get update && apt-get install -y sqlite3first.Once the fix is confirmed, exit the container and run
docker compose up -d.
Managed host: use the separate maintenance checkout procedure at the exact active tag or commit, outside release directories. Stop the API first and target the persistent database and evidence paths explicitly. Don't install development dependencies into current/ or start another API from that checkout.
Suspected compromise or a leaver
Symptom: unexpected sign-ins, a leaked credential, or a company-login user (a leaver, a compromised upstream identity) must be cut off now.
Cause: disabling an identity at the provider blocks new sign-ins, not existing CapacityLens sessions. An active session lasts up to its fixed twelve hours; an inactive one expires after thirty minutes. Signing out of CapacityLens ends only its own session, not the session at the company-login provider.
Fix:
- For a suspected compromise, restrict public access at the proxy. Preserve, without altering, the database with its
-wal/-shmsidecars, the0600-mode current and rotated audit files, forwarded security events and relevant proxy logs. On Docker Compose, use the stopped-volume preservation step with the actual database and backup volumes and a separate protected destination; don't copy a host path or only the database file. - For a company-login identity, disable it and revoke its sessions at the provider.
- In Team & access, select Revoke sessions for the person in every affected installation.
- Only if every session must end at once, rotate the affected provider client secret and
CAPACITYLENS_SECRET; this signs everyone out. - Review identities, memberships, invitations, audit events and provider logs. Don't correlate or merge identities by email address alone.
- Keep access restricted until the integrity review is complete, and record the recovery decision against the evidence before re-enabling it.
- Follow your organisation's notification and disclosure obligations.
Check: record the actual containment time against the twelve-hour/thirty-minute maximum; revocation isn't instant across sessions.
The sole Owner has lost their password
Symptom: the only active Owner of a password-mode instance can't sign in.
Cause: Admins can't reset an Owner's credential and there's no public reset endpoint; a backup holds the same credential. An operator tool issues a normal single-use, 24-hour reset link instead.
Fix:
Preserve the database file with its
-wal/-shmcompanions and record checksums.Stop the application. The tool refuses to run while another process holds the database.
Using the release that most recently started the database, with the instance's environment present, run the tool from the release archive. Native install (
--pipekeeps the link out of the journal):bashsudo systemctl stop capacitylens sudo systemd-run --pipe --wait --quiet --uid=capacitylens -p EnvironmentFile=/etc/capacitylens.env /usr/bin/env node /opt/capacitylens/current/server/dist/reset-owner-password.mjs /var/lib/capacitylens/capacitylens.db owner@example.com --confirm-server-stopped; echo "exit status $?"A non-zero exit status means no link was issued; the message above it says why. Managed host, after stopping the background process, from the command box or a shell in the site's root folder:
bashcd current && node --env-file=../.env server/dist/reset-owner-password.mjs /home/forge/capacitylens-data/capacitylens.db owner@example.com --confirm-server-stoppedSource checkout or the Docker throwaway container:
pnpm --filter capacitylens-server reset:owner-password -- /absolute/path/to/capacitylens.db owner@example.com --confirm-server-stoppedwithCAPACITYLENS_MODE=password-only,CAPACITYLENS_SECRETandCAPACITYLENS_PUBLIC_URLset.The JSON output contains the reset link, and that link is the secret. Deliver it over a channel you trust; never store it in tickets or logs.
Restart the application. The Owner opens the link, sets a new password (this revokes every existing session) and signs in.
The tool refuses a missing or ambiguous identity, anyone who isn't the sole active Owner of at least one company (they have an in-product reset), an older schema and non-password modes.
Check: the audit event identity.owner_recovery_issued (a digest, never the token) reaches its destination.
Membership changes fail with nextOwnershipTransferRevision
Symptom: role, status, removal or ownership-transfer changes fail; the log says nextOwnershipTransferRevision cannot advance safely, or the stored revision is not a non-negative integer.
Cause: a live ownership-transfer row has an exhausted (9007199254740991) or corrupt revision.
Fix:
- Restore a verified snapshot if it has the correct row and you accept losing later writes.
- Otherwise follow Recover a blocked ownership transfer. It cancels only the request the Owner approves.
- If the company also has no active Owner, resolve the revision first, then follow A company has no Owner if still needed.
Check: the originally intended membership change succeeds.
Malformed or corrupted audit outbox record
Symptom: startup stops before listening, or logs point at the audit outbox.
Cause: the oldest outbox payload is corrupt or was altered, and fails closed.
Fix: never delete or update an outbox row with ad hoc SQL.
Stop the API. Preserve the database with its
-wal/-shmfiles and both audit JSONL generations, and take checksums. Work only on a verified copy of the stopped original.Inspect the oldest row; its raw payload isn't printed:
shpnpm --filter capacitylens-server recover:audit-outbox -- inspect /absolute/path/to/capacitylens.dbvalid: don't quarantine; investigate the audit sink.invalid-jsonorinvalid-payload: note the row ID, byte count and SHA-256 digest, and escalate under your incident and audit-retention policy.With explicit approval, quarantine only that still-current malformed head:
shpnpm --filter capacitylens-server recover:audit-outbox -- quarantine /absolute/path/to/capacitylens.db expected-head-id /absolute/path/to/evidence.jsonIt refuses a valid or already-changed head, and refuses to overwrite an existing evidence file. It saves the raw row and digest to a mode-
0600evidence file, then deletes that one row. Retain the evidence under your audit policy.Restart the API.
Check: audit, auditPending and the JSONL destination show the backlog draining. Inspect again only if another malformed head appears.
CorruptAccountCommandStateError, or command-status stuck at reconciliation_required
Symptom: an account command (one involving an identity provider) returns a generic 500 with CorruptAccountCommandStateError and a command id in the log, or its status stays reconciliation_required.
Cause: the provider outcome, or a compensating action, couldn't be proven. Treat it as an integrity incident.
Fix:
Stop retrying the command with a new idempotency key.
Inspect the account audit event and the
account_commandsrepair coordinates, then verify the actual membership, session, reset-ceremony or provisional-identity state.Complete or undo the intended effect with the normal administrative control. Record an incident reference with no credentials, tokens or personal data.
Stop the application. Using the release that most recently started the database, close the repaired record:
shpnpm --filter capacitylens-server exec tsx scripts/reconcile-account-command.ts /absolute/path/to/capacitylens.db application-id command-id operator-referenceIt stores only a digest of your reference, and refuses records not awaiting reconciliation and an older schema.
Check: the status is compensated. Keep the audit evidence; if the operation is still needed, retry with a new command identity.
Don't close a malformed-metadata row until its metadata is recovered from a known-good copy or every external effect is established from provider and audit evidence. A legacy row whose resultJson is SQL NULL is the exception.
Erasure refuses with TenantErasureIntegrityError
Symptom: deleting a company fails with a generic API error, and the server log shows TenantErasureIntegrityError.
Cause: a corrupt relationship would cascade into another company's data.
Fix:
- Stop the application. Preserve the database with its
-wal/-shmsidecars and both audit-log generations, to a separate protected destination if space is short. - Identify the reported parent/child edge. Report it through your approved incident tracker with redacted metadata only (ids, table names, timestamps, checksums).
- Restore a verified snapshot with the correct edge (restore procedure) if you accept losing later writes. Otherwise repair the relationship only against an authoritative source. Don't disable foreign keys, run ad hoc SQL or delete the child row to make erasure pass.
Check: deleting the company succeeds.
A company has no Owner
Symptom: Team & access shows no Owner, or startup logs a security event about an ownership repair.
Cause: the company lost its only active Owner. An upgrade may repair this by promoting the highest-tier active member (oldest membership wins ties; a Viewer only if every active member is one), logging a security event.
Fix:
After an upgrade, find the promotion event and confirm the promoted person is appropriate. To change who holds the role, use the in-app ownership transfer.
If the company remains ownerless, preserve the database and audit logs and stop the server. While the deployment remains in
self-hosted-mixedwithCAPACITYLENS_MODE=password-and-ssoand a company provider configured, assign an existing active member:bashpnpm --filter capacitylens-server cutover:repair -- /path/to/capacitylens.db \ assign-workspace-owner <company-id> <member-email> --confirm-server-stoppedIt refuses to run without
--confirm-server-stopped, and takes an exclusive database lock. It checks the company has no active Owner and the email matches exactly one active member, then records the change in the audit outbox and ends any pending ownership transfer.If those checks don't match the incident, preserve the evidence and restore a compatible backup or escalate; don't edit the database by hand.
Check: Team & access shows one Owner.
Owner password recovery is for an Owner who exists but can't sign in; ownership-transfer recovery only handles a pending transfer.
Disk-full or a failed snapshot
Symptom: SQLITE_FULL, backup FAILED in the log, deep health stuck at degraded or pending, or no free blocks or inodes.
Cause: the host has run out of space or inodes, or a database, audit or backup path isn't writable.
Fix:
- Stop write traffic before any cleanup: stop the Compose API or the systemd service.
- Preserve the database with its
-wal/-shmsidecars, the current and rotated audit logs and the latest known-good snapshot to separate protected or off-host storage, never another copy on the full host. Recorddf -h,df -iand the exact health/log messages. Never delete the only known-good snapshot or truncate an audit log to make room. - Free space only from material already retained elsewhere, such as an old release directory or an older snapshot with a verified off-host copy. Keep the configured retention policy.
- Confirm the database, audit and backup paths are writable, then restart the service.
Check: deep health recovers and one complete scheduled snapshot succeeds. If the next snapshot fails, the database reports an integrity error, or SQLite cannot reopen the database, stop the service and escalate as a data-integrity incident. Don't retry writes or run ad hoc SQLite repairs; follow Backups and restore once you know what data a verified snapshot would lose.