Skip to content

Owner Recovery

david edited this page Sep 4, 2026 · 4 revisions

Owner Recovery

This page covers what the workspace owner recovery key is for, a known issue in versions before v0.1.1-alpha that could cause repeated lockouts, and how to safely reclaim ownership — including a direct-database path for self-hosted Supabase if the in-app flow keeps failing.

What the recovery key is

Every shared inventory has exactly one owner. The recovery key (shown once, as a QR code and a package you can save or download) lets a new device reclaim the owner role if every device that held it is lost. Using it is a real ownership transfer: it revokes every other device currently claiming owner and issues a brand new recovery key.

Save the whole package — not just the bare key text. The package also carries the workspace ID, which recovery needs; the bare key alone can't be used to reclaim access.

Recovery JSON package

The package is a small JSON object. Keep the workspace_id exactly as issued and put the current key in recovery_key:

{
  "workspace_id": "<WORKSPACE_ID>",
  "recovery_key": "<CURRENT_RECOVERY_KEY>"
}

When a key is rotated outside the app, replace only the value of recovery_key in your saved JSON file. Do not change the workspace ID, rename either property, paste the shell command, or paste a JSON code fence. The app's Recover ownership field expects the raw JSON object.

Known issue (fixed in v0.1.1-alpha)

Versions before v0.1.1-alpha could trigger that ownership-transfer flow automatically and silently whenever a device's Supabase session simply failed to refresh — a routine event, not actual device loss. If you had owner access configured on more than one device or app install pointed at the same workspace, this could cascade: the first one to hit a refresh failure would silently evict every other owner device and rotate the recovery key without ever showing you the new one, leaving previously-valid saved keys rejected.

Symptoms: "Recovery key or inventory ID is invalid" even with a key you just generated; ownership appearing to flip between devices on its own.

v0.1.1-alpha and later only ever transfers ownership from an explicit, user-initiated Recover ownership action — never automatically in the background. If you're on an older build and hit this, update first; it removes the underlying cause.

Safe recovery workflow

If you're locked out on one device and have a working device elsewhere:

  1. Close every other Inventorinator instance you have running, on every device — including any test/debug builds. As long as more than one install has owner access cached, whichever reconnects first can win a race and invalidate the key you're about to use.
  2. On the one device you're keeping: open Remote Sync → Ownership & recovery to get a current key, or use your most recently saved recovery package.
  3. On the locked-out device: Remote Sync → Disconnect this device (this only clears local cached credentials — shared data is not touched), then Join existing inventory → Recover ownership, and paste the package from step 2.
  4. Do steps 2–3 back to back, without leaving the working device idle for long — its own background sync can still rotate the key again if you wait.

A rejected recovery attempt doesn't cost you anything — the server checks the key before it writes anything, so failed attempts are safe to retry.

Fallback: rotate the key with SQL instead of the app

If the app's Ownership & recovery screen is unreachable, or you don't trust the timing enough to risk another race, rotate the key directly at the database instead of through the app. This guarantees the key you get is the true current one at that exact instant, with nothing else able to race you between generating it and reading it:

First identify the current owner user ID:

docker exec -i supabase-db psql -U postgres -d postgres -Atc \
  "select user_id from public.inventorinator_workspace_members
   where workspace_id = '<WORKSPACE_ID>' and role = 'owner';"

Then rotate the key while presenting that owner identity to the database function. This prints only the fresh key:

docker exec -i supabase-db psql -U postgres -d postgres -Atc \
  "begin;
   set local request.jwt.claim.sub = '<OWNER_USER_ID>';
   select public.rotate_inventorinator_recovery_key('<WORKSPACE_ID>');
   commit;"

Copy the returned key and replace only recovery_key in your saved JSON package. For example:

{
  "workspace_id": "<WORKSPACE_ID>",
  "recovery_key": "<KEY_RETURNED_BY_DOCKER>"
}

Paste the raw JSON object into Recover ownership on the locked-out device right away. A successful recovery rotates the key again and returns a new JSON package in the app — save that new package and discard the older one. Don't let another working device sync between rotation and recovery, and do not paste the old key again after recovery succeeds.

If it keeps failing (self-hosted, direct database recovery)

If you have psql or Docker access to your self-hosted Supabase instance, you can bypass the client entirely — useful if multiple installs have been racing and you're not sure which key is actually current.

See what the server currently thinks (swap in your workspace ID and container name — the default container is supabase-db, matching install-or-update.sh):

docker exec -i supabase-db psql -U postgres -d postgres -c \
  "select workspace_id, user_id, role, device_name, last_seen_at
   from public.inventorinator_workspace_members
   where workspace_id = '<WORKSPACE_ID>'
   order by last_seen_at desc;"

This lists every device ever registered and its current role. Only one row should say owner. If the device attached to that row no longer has a live session (for example, it shows up again later under a different, lower-role row — a sign it lost its session and rejoined from scratch), the "owner" role on record may not be usable from any device you currently have open.

Rather than fight the recovery-key flow further, promote the device you're actually using right now directly:

docker exec -i supabase-db psql -U postgres -d postgres -c \
  "update public.inventorinator_workspace_members
   set role = 'owner'
   where workspace_id = '<WORKSPACE_ID>' and user_id = '<USER_ID_TO_PROMOTE>';
   update public.inventorinator_workspace_members
   set role = 'admin'
   where workspace_id = '<WORKSPACE_ID>' and role = 'owner'
     and user_id <> '<USER_ID_TO_PROMOTE>';
   update public.inventorinator_workspaces
   set created_by = '<USER_ID_TO_PROMOTE>'
   where id = '<WORKSPACE_ID>';"

Pick <USER_ID_TO_PROMOTE> from the device list above — the row for the device you're currently using and trust. That device will pick up owner status the next time it syncs; no recovery key needed. Only ever run this against a workspace you own.

Preventing this

  • Save the full recovery package (QR or downloaded .json), not just the bare key, somewhere durable like a password manager.
  • Avoid keeping more than one install signed in with owner access long-term — add other devices as admin/manager/editor/builder instead, and reserve owner for the one device you actually use to manage the workspace.
  • Update to v0.1.1-alpha or later, which removes the automatic silent recovery path described above.

Clone this wiki locally