Upgrading to 1.3.0
Keyper 1.3.0 changes how you sign in and how your vault key is stored. If you already use Keyper with Supabase, there is a one-time database update to run before the new version will open your vault.
About five minutes to read, about five minutes to do. Reading first is what makes the difference.
Do I need this?
Section titled “Do I need this?”| You use | Need this guide? |
|---|---|
| Supabase, vault created before 1.3.0 | Yes |
| SQLite (local) | No |
| Neon | No |
| Brand new install | No — run sql/supabase-setup.sql instead |
Keyper detects this for you too. If you open 1.3.0 and see “Your database needs a one-time update”, you are in the right place.
What is changing
Section titled “What is changing”Before: you typed a username on the unlock screen and Keyper trusted it. The database handed over rows to anyone who asked.
After: you sign in with an email and password, and the database checks who you are on every request.
Also: your vault key used to be stored in a form the server could read directly. Now it is encrypted under your master passphrase, so a copy of the database cannot decrypt anything on its own.
What this does not do
Section titled “What this does not do”- Does not delete any credentials
- Does not re-encrypt anything, so it is fast even with a large vault
- Does not change your master passphrase
- Does not touch your categories or tags
Two things to know first
Section titled “Two things to know first”You will have two secrets, not one
Section titled “You will have two secrets, not one”This is the part that trips people up.
| | Account password | Master passphrase | |---|---|---| | Created | During this migration | You already have it | | What it does | Gets you your rows | Decrypts them | | Can be reset? | Yes, by email | No, never |
You create a new account password during this migration. Your existing master passphrase does not change, and you will still need it.
Your master passphrase becomes unrecoverable
Section titled “Your master passphrase becomes unrecoverable”Older Keyper let you reset a forgotten passphrase by editing the database. That only worked because the vault key sat separately in usable form, which is exactly what is being fixed.
Overview
Section titled “Overview”1. Back up Supabase dashboard2. Turn on email sign-in Supabase dashboard3. Create your account Supabase dashboard4. Run 01-check.sql SQL Editor (reads only)5. Run 02-claim-your-data.sql SQL Editor ← the only edit6. Run 03-apply-security.sql SQL Editor7. Sign in and unlock Keyper8. Run 04-check-key.sql SQL Editor (reads only)9. Run 05-remove-old-key.sql SQL EditorStep 1 — Back up your database
Section titled “Step 1 — Back up your database”Supabase dashboard → Database → Backups.
Do this even though the migration is careful. It costs nothing and means a mistake is never permanent.
Step 2 — Turn on email sign-in
Section titled “Step 2 — Turn on email sign-in”Authentication → Providers → Email → enable.
Keyper signs you in to a real account now. Without this, nobody can create one.
Step 3 — Create your account
Section titled “Step 3 — Create your account”Authentication → Users → Add user. Real email address, strong password.
Step 4 — Find your account ID
Section titled “Step 4 — Find your account ID”Paste this into SQL Editor → New query and run it. It only reads, so it is safe to run as often as you like.
01-check.sql Reads only. Tells you the account UUID you need next, and whether any of the migration has already been applied.
Show the SQL
-- =====================================================
-- KEYPER 1.3.0 MIGRATION — STEP 1 of 5: CHECK
-- =====================================================
--
-- Safe to run. This only reads. It changes nothing.
--
-- Paste the whole file into the Supabase SQL Editor and run it.
--
-- It tells you which account UUID to use in step 2, and whether any of the
-- migration has already been applied.
-- =====================================================
SELECT 'Accounts you can migrate to' AS check,
COALESCE(string_agg(email || ' -> ' || id, E'\n'),
'NONE — create one under Authentication > Users') AS result
FROM auth.users
UNION ALL
SELECT 'Migration started?',
CASE WHEN COUNT(*) = 3 THEN 'YES — step 2 already applied'
WHEN COUNT(*) = 0 THEN 'NO — nothing applied yet, start at step 2'
ELSE 'PARTLY — owner_id on ' || COUNT(*) || ' of 3 tables' END
FROM information_schema.columns
WHERE table_schema = 'public' AND column_name = 'owner_id'
AND table_name IN ('credentials', 'vault_config', 'categories')
UNION ALL
SELECT 'Your data',
'credentials=' || (SELECT COUNT(*) FROM credentials)
|| ' vault_config=' || (SELECT COUNT(*) FROM vault_config)
|| ' categories=' || (SELECT COUNT(*) FROM categories)
UNION ALL
SELECT 'Usernames in use',
(SELECT COALESCE(string_agg(DISTINCT user_id, ', '), 'none') FROM (
SELECT user_id FROM credentials
UNION SELECT user_id FROM vault_config
UNION SELECT user_id FROM categories
) u)
UNION ALL
SELECT 'Vault key state',
COALESCE((SELECT CASE
WHEN wrapped_dek IS NOT NULL AND raw_dek IS NULL THEN 'new format — step 4 done'
WHEN raw_dek IS NOT NULL THEN 'original format — step 4 not done yet'
ELSE 'no key found' END
FROM vault_config LIMIT 1), 'no vault_config row')
UNION ALL
SELECT 'Access rules',
(SELECT CASE
WHEN COUNT(*) FILTER (WHERE 'authenticated' = ANY(roles)) = 12 THEN 'new owner-scoped rules active — step 3 done'
WHEN COUNT(*) = 0 THEN 'no policies found'
ELSE 'original rules still active (' || COUNT(*) || ' policies)' END
FROM pg_policies
WHERE schemaname = 'public'
AND tablename IN ('credentials', 'vault_config', 'categories'));
-- =====================================================
-- WHAT TO DO WITH THIS OUTPUT
-- =====================================================
--
-- "Accounts you can migrate to"
-- Copy the UUID after your email address. That is what step 2 needs.
-- If it says NONE, go to Authentication > Providers, enable Email, then
-- Authentication > Users > Add user. Then run this file again.
--
-- Note: this is NOT any of the id values you see in the credentials or
-- vault_config tables. Those are row ids, one per row.
--
-- "Migration started?"
-- NO -> continue to 02-claim-your-data.sql
-- PARTLY -> continue to 02-claim-your-data.sql, it is safe to re-run
-- YES -> step 2 is done; check "Access rules" for whether step 3 is too
--
-- NEXT: 02-claim-your-data.sql
-- =====================================================
You will get something like:
Accounts you can migrate to | you@example.com -> a1b2c3d4-e5f6-7890-abcd-ef1234567890Migration started? | NO — nothing applied yet, start at step 2Your data | credentials=25 vault_config=1 categories=8Usernames in use | sizzlebopVault key state | original format — step 4 not done yetAccess rules | original rules still active (12 policies)Copy the UUID after your email address.
If “Accounts you can migrate to” says NONE, go back to steps 2 and 3.
Make a note of the “Your data” numbers. You will check them in the next step.
Step 5 — Claim your data
Section titled “Step 5 — Claim your data”02-claim-your-data.sql needs one edit Adds an ownership column and marks your existing rows as yours. Does not change credential data.
Show the SQL
-- =====================================================
-- KEYPER 1.3.0 MIGRATION — STEP 2 of 5: CLAIM YOUR DATA
-- =====================================================
--
-- ⚠️ ONE EDIT NEEDED before you run this.
--
-- Find this line below and replace the zeros with your account UUID from
-- step 1 (01-check.sql):
--
-- target_owner UUID := '00000000-0000-0000-0000-000000000000';
--
-- Then paste the whole file into the SQL Editor and run it.
--
-- What it does: adds an owner column to your three tables and marks your
-- existing rows as belonging to your account. It does not change any
-- credential data and does not touch your encryption key.
--
-- Safe to re-run. If you get it wrong, nothing is applied and it tells you why.
-- =====================================================
-- Add the ownership column. Nullable for now; step 3 tightens it once every
-- row has an owner.
ALTER TABLE credentials ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE vault_config ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
ALTER TABLE categories ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
DO $$
DECLARE
-- ⬇⬇⬇ PASTE YOUR ACCOUNT UUID FROM STEP 1 HERE ⬇⬇⬇
target_owner UUID := '00000000-0000-0000-0000-000000000000';
-- Leave this as NULL unless several people share this database. If they do,
-- set it to one legacy username (for example 'sizzlebop') and run this file
-- once per person, using each person's own account UUID.
only_username TEXT := NULL;
n_credentials INT;
n_vault INT;
n_categories INT;
owner_email TEXT;
BEGIN
IF target_owner = '00000000-0000-0000-0000-000000000000'::uuid THEN
RAISE EXCEPTION
E'\n\nThe account UUID has not been filled in yet.\n\n'
'Run 01-check.sql and copy the UUID shown next to your email address,\n'
'then paste it into target_owner near the top of this file.\n\n'
'Nothing has been changed.\n';
END IF;
SELECT email INTO owner_email FROM auth.users WHERE id = target_owner;
IF owner_email IS NULL THEN
RAISE EXCEPTION
E'\n\nNo account exists with id %.\n\n'
'Re-run 01-check.sql and copy the UUID exactly as shown.\n'
'If no accounts are listed, create one under\n'
'Authentication > Users > Add user.\n\n'
'Nothing has been changed.\n', target_owner;
END IF;
-- One account holds one vault. Each legacy username has its own vault_config
-- with its own encryption key, and the credentials under each are encrypted
-- with a different key, so two vaults cannot merge into one account.
SELECT COUNT(*) INTO n_vault
FROM vault_config
WHERE owner_id = target_owner
OR (owner_id IS NULL AND (only_username IS NULL OR user_id = only_username));
IF n_vault > 1 THEN
RAISE EXCEPTION
E'\n\nThis database holds % separate vaults, and one account can hold\n'
'only one of them.\n\n'
'Each username has its own encryption key, so their credentials cannot\n'
'be merged into a single account.\n\n'
'See which usernames have a vault:\n'
' SELECT user_id, created_at FROM vault_config ORDER BY created_at;\n\n'
'Then either:\n'
' a) create one account per username, and run this file once per person\n'
' with only_username set to that username; or\n'
' b) if you only want to keep one, set only_username to that username.\n\n'
'Nothing has been changed.\n', n_vault;
END IF;
UPDATE credentials SET owner_id = target_owner
WHERE owner_id IS NULL AND (only_username IS NULL OR user_id = only_username);
GET DIAGNOSTICS n_credentials = ROW_COUNT;
UPDATE vault_config SET owner_id = target_owner
WHERE owner_id IS NULL AND (only_username IS NULL OR user_id = only_username);
GET DIAGNOSTICS n_vault = ROW_COUNT;
UPDATE categories SET owner_id = target_owner
WHERE owner_id IS NULL AND (only_username IS NULL OR user_id = only_username);
GET DIAGNOSTICS n_categories = ROW_COUNT;
RAISE NOTICE E'\n\nAssigned to %:\n'
' % credential(s)\n'
' % vault config(s)\n'
' % category/ies\n\n'
'Check those numbers look right, then run 03-apply-security.sql\n',
owner_email, n_credentials, n_vault, n_categories;
END $$;
-- Anything still unowned? This should come back empty.
-- If it lists rows, run this file again with only_username set to NULL.
SELECT 'credentials' AS table_name, user_id, COUNT(*) AS still_unowned FROM credentials WHERE owner_id IS NULL GROUP BY 1, 2
UNION ALL
SELECT 'vault_config' AS table_name, user_id, COUNT(*) AS still_unowned FROM vault_config WHERE owner_id IS NULL GROUP BY 1, 2
UNION ALL
SELECT 'categories' AS table_name, user_id, COUNT(*) AS still_unowned FROM categories WHERE owner_id IS NULL GROUP BY 1, 2;
-- =====================================================
-- NEXT: 03-apply-security.sql
-- =====================================================
Expected output:
NOTICE: Assigned to you@example.com: 25 credential(s) 1 vault config(s) 8 category/iesThe query at the bottom should return no rows. If it lists anything, run the
script again with only_username as NULL.
Step 6 — Apply the new access rules
Section titled “Step 6 — Apply the new access rules”No edits. Paste and run.
03-apply-security.sql Swaps over the database access rules so each account reaches only its own rows. Stops safely if step 5 did not finish.
Show the SQL
-- =====================================================
-- KEYPER 1.3.0 MIGRATION — STEP 3 of 5: APPLY THE NEW ACCESS RULES
-- =====================================================
--
-- No edits needed. Paste the whole file into the SQL Editor and run it.
--
-- What it does: replaces the database access rules so that each account can
-- only reach its own rows, and requires a signed-in session. It does not touch
-- any credential data and does not touch your encryption key.
--
-- Safe to re-run. If step 2 was not finished, this stops without changing
-- anything and tells you what is missing.
--
-- After this runs, the app needs you to sign in. That is expected.
-- =====================================================
DO $$
DECLARE
unclaimed INT;
have_cols INT;
tbl TEXT;
BEGIN
-- Has step 2 been run at all? Without the owner column there is nothing here
-- to work with, and the checks below would fail with a raw Postgres error.
SELECT COUNT(*) INTO have_cols
FROM information_schema.columns
WHERE table_schema = 'public' AND column_name = 'owner_id'
AND table_name IN ('credentials', 'vault_config', 'categories');
IF have_cols < 3 THEN
RAISE EXCEPTION
E'\n\nSTOPPED: this database has not had step 2 applied yet.\n\n'
'Run 02-claim-your-data.sql first. It adds the owner column and marks\n'
'your existing rows as yours.\n\n'
'Nothing has been changed. Your current setup still works.\n';
END IF;
-- Refuse to continue while any row has no owner. The new rules match rows by
-- owner, so an unowned row would still be on disk but invisible to the app.
SELECT (SELECT COUNT(*) FROM credentials WHERE owner_id IS NULL)
+ (SELECT COUNT(*) FROM vault_config WHERE owner_id IS NULL)
+ (SELECT COUNT(*) FROM categories WHERE owner_id IS NULL)
INTO unclaimed;
IF unclaimed > 0 THEN
RAISE EXCEPTION
E'\n\nSTOPPED: % row(s) do not have an owner yet.\n\n'
'Run 02-claim-your-data.sql first, with only_username left as NULL so it\n'
'claims everything.\n\n'
'Nothing has been changed. Your current setup still works.\n', unclaimed;
END IF;
-- Every row has an owner, so the column can be required from now on.
FOREACH tbl IN ARRAY ARRAY['credentials', 'vault_config', 'categories'] LOOP
EXECUTE format('ALTER TABLE public.%I ALTER COLUMN owner_id SET NOT NULL', tbl);
EXECUTE format('ALTER TABLE public.%I ALTER COLUMN owner_id SET DEFAULT auth.uid()', tbl);
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON public.%I(owner_id)',
'idx_' || tbl || '_owner_id', tbl);
END LOOP;
-- One vault per account, and category names unique per account, replacing the
-- old per-username versions.
ALTER TABLE vault_config DROP CONSTRAINT IF EXISTS vault_config_user_id_key;
ALTER TABLE categories DROP CONSTRAINT IF EXISTS categories_user_id_name_key;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'vault_config_owner_id_key') THEN
ALTER TABLE vault_config ADD CONSTRAINT vault_config_owner_id_key UNIQUE (owner_id);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'categories_owner_id_name_key') THEN
ALTER TABLE categories ADD CONSTRAINT categories_owner_id_name_key UNIQUE (owner_id, name);
END IF;
FOREACH tbl IN ARRAY ARRAY['credentials', 'vault_config', 'categories'] LOOP
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', tbl);
-- Clear whatever is currently attached, whatever it happens to be called.
EXECUTE (
SELECT COALESCE(string_agg(format('DROP POLICY IF EXISTS %I ON public.%I;', policyname, tbl), ' '), '')
FROM pg_policies WHERE schemaname = 'public' AND tablename = tbl
);
-- TO authenticated means a request with no session matches no policy.
-- owner_id = auth.uid() keeps each account to its own rows. The WITH CHECK
-- clauses stop a row being written under, or moved to, another owner.
EXECUTE format($f$
CREATE POLICY %I ON public.%I
FOR SELECT TO authenticated USING (owner_id = (SELECT auth.uid()));
CREATE POLICY %I ON public.%I
FOR INSERT TO authenticated WITH CHECK (owner_id = (SELECT auth.uid()));
CREATE POLICY %I ON public.%I
FOR UPDATE TO authenticated USING (owner_id = (SELECT auth.uid()))
WITH CHECK (owner_id = (SELECT auth.uid()));
CREATE POLICY %I ON public.%I
FOR DELETE TO authenticated USING (owner_id = (SELECT auth.uid()));
$f$,
tbl || '_select_policy', tbl,
tbl || '_insert_policy', tbl,
tbl || '_update_policy', tbl,
tbl || '_delete_policy', tbl
);
END LOOP;
-- Also inside this block: revoking anon while the old rules were still in
-- place would break the running app before the new rules were ready.
EXECUTE 'REVOKE ALL ON credentials, vault_config, categories FROM anon';
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON credentials, vault_config, categories TO authenticated';
RAISE NOTICE E'\n\nDone. Access rules replaced on all three tables.\n\n'
'Next: open Keyper, sign in with your account, and unlock with your\n'
'existing master passphrase. Then run 04-check-key.sql.\n';
END $$;
-- This function read the credentials table in a way that ignored access rules.
DROP FUNCTION IF EXISTS public.get_credential_stats();
-- Every row below must say SCOPED.
SELECT
tablename,
policyname,
CASE
WHEN 'anon' = ANY(roles) OR 'public' = ANY(roles) THEN 'REACHABLE WITHOUT SIGNING IN'
WHEN COALESCE(qual, 'true') = 'true' AND COALESCE(with_check, 'true') = 'true' THEN 'NOT SCOPED'
ELSE 'SCOPED'
END AS verdict
FROM pg_policies
WHERE schemaname = 'public'
AND tablename IN ('credentials', 'vault_config', 'categories')
ORDER BY tablename, policyname;
-- This should come back empty.
SELECT table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'anon'
AND table_schema = 'public'
AND table_name IN ('credentials', 'vault_config', 'categories');
-- =====================================================
-- NEXT: open Keyper, sign in, unlock with your existing master passphrase.
-- Then run 04-check-key.sql.
-- =====================================================
Expected output:
NOTICE: Done. Access rules replaced on all three tables.Then a table where every row says SCOPED, and a final query returning no
rows.
Step 7 — Sign in and unlock
Section titled “Step 7 — Sign in and unlock”Open Keyper. If you are on the upgrade screen, press Re-check database.
- Sign in with the account from step 3
- Unlock with your existing master passphrase, not the account password
Your credentials should all be there. Keyper moves your vault key to the new format the moment you unlock. Nothing is re-encrypted, so it is instant.
Step 8 — Confirm the key moved
Section titled “Step 8 — Confirm the key moved”04-check-key.sql Reads only. Confirms your vault key moved to the new format.
Show the SQL
-- =====================================================
-- KEYPER 1.3.0 MIGRATION — STEP 4 of 5: CHECK THE KEY MOVED
-- =====================================================
--
-- Safe to run. This only reads. It changes nothing.
--
-- Run this AFTER you have opened Keyper, signed in, and unlocked your vault
-- with your existing master passphrase.
--
-- When you unlock, Keyper moves your vault key to the new format on its own.
-- Nothing is re-encrypted, so it happens instantly. This file just confirms it.
-- =====================================================
SELECT
user_id AS vault,
CASE
WHEN wrapped_dek IS NOT NULL AND raw_dek IS NULL
THEN 'DONE — ready for step 5'
WHEN wrapped_dek IS NOT NULL AND raw_dek IS NOT NULL
THEN 'DONE — ready for step 5 (old copy still present, step 5 removes it)'
ELSE
'NOT YET — open Keyper and unlock this vault first. Do not run step 5.'
END AS status
FROM vault_config
ORDER BY user_id;
-- =====================================================
-- WHAT TO DO WITH THIS OUTPUT
-- =====================================================
--
-- Every row says DONE
-- -> continue to 05-remove-old-key.sql
--
-- Any row says NOT YET
-- -> open Keyper, sign in as that account, and unlock with its master
-- passphrase. Then run this file again.
-- Do not run step 5 until every row says DONE. Step 5 removes the old
-- key, and a vault that has not moved across still needs it.
--
-- NEXT: 05-remove-old-key.sql
-- =====================================================
Expected output:
vault | statussizzlebop | DONE — ready for step 5Step 9 — Remove the old key copy
Section titled “Step 9 — Remove the old key copy”The last one. No edits.
05-remove-old-key.sql Removes the old copy of your vault key. Checks first and refuses if any vault has not moved across, so it cannot run too early.
Show the SQL
-- =====================================================
-- KEYPER 1.3.0 MIGRATION — STEP 5 of 5: REMOVE THE OLD KEY COPY
-- =====================================================
--
-- No edits needed. Paste the whole file into the SQL Editor and run it.
--
-- What it does: removes the two old columns that stored your vault key in its
-- previous form, now that Keyper has moved it to the new one.
--
-- This one does remove data, so it checks first. If any vault has not moved to
-- the new format yet, it stops and changes nothing. You cannot run this too
-- early by accident.
--
-- Run 04-check-key.sql first if you have not already.
-- =====================================================
DO $$
DECLARE
not_ready INT;
names TEXT;
BEGIN
-- Nothing to do if a previous run already removed the columns.
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'vault_config'
AND column_name IN ('raw_dek', 'bcrypt_hash')
) THEN
RAISE NOTICE E'\n\nAlready done. The old columns are gone.\n\n'
'Your migration is complete.\n';
RETURN;
END IF;
SELECT COUNT(*), string_agg(user_id, ', ')
INTO not_ready, names
FROM vault_config
WHERE wrapped_dek IS NULL;
IF not_ready > 0 THEN
RAISE EXCEPTION
E'\n\nSTOPPED: % vault(s) have not moved to the new key format yet: %\n\n'
'Open Keyper, sign in as that account, and unlock with its master\n'
'passphrase. Keyper moves the key across automatically.\n\n'
'Then run 04-check-key.sql to confirm, and try this file again.\n\n'
'Nothing has been changed. Removing the old key now would leave that\n'
'vault unreadable.\n', not_ready, names;
END IF;
ALTER TABLE vault_config DROP COLUMN IF EXISTS raw_dek;
ALTER TABLE vault_config DROP COLUMN IF EXISTS bcrypt_hash;
RAISE NOTICE E'\n\nDone. Old key columns removed.\n\n'
'Your migration is complete. Your vault key is now stored only in\n'
'a form your master passphrase can open.\n';
END $$;
-- Confirm the columns are gone. This should come back empty.
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'vault_config'
AND column_name IN ('raw_dek', 'bcrypt_hash');
-- =====================================================
-- MIGRATION COMPLETE
-- =====================================================
--
-- What is different now:
--
-- - You sign in to Keyper with an account, then unlock with your master
-- passphrase. Two separate steps.
-- - Each account reaches only its own rows, checked by the database.
-- - Your vault key is stored only in a form your passphrase can open. Keep a
-- copy of that passphrase somewhere safe: it is now the only way in.
-- =====================================================
Expected output:
NOTICE: Done. Old key columns removed. Your migration is complete.You are finished. 🎉
Troubleshooting
Section titled “Troubleshooting”Every one of these is recoverable. None of them lose data.
”The account UUID has not been filled in yet”
Section titled “”The account UUID has not been filled in yet””You ran 02-claim-your-data.sql without doing the edit. Go back to step 5.
”No account exists with id …”
Section titled “”No account exists with id …””The UUID is wrong, or no account exists yet. Re-run 01-check.sql and copy the
UUID exactly. If it lists no accounts, do steps 2 and 3.
”this database has not had step 2 applied yet”
Section titled “”this database has not had step 2 applied yet””You ran 03-apply-security.sql before 02-claim-your-data.sql. Run them in
order. Nothing was changed.
”STOPPED: N row(s) do not have an owner yet”
Section titled “”STOPPED: N row(s) do not have an owner yet””02-claim-your-data.sql did not match all your rows, usually because
only_username was set. Run it again with only_username as NULL.
Step 5 says “0 credential(s)”
Section titled “Step 5 says “0 credential(s)””Either the rows were already claimed by an earlier run, or only_username does
not match your actual username. Run 01-check.sql and look at “Usernames in
use”.
Keyper offers to create a new vault instead of asking for my passphrase
Section titled “Keyper offers to create a new vault instead of asking for my passphrase”Your vault_config row was not claimed. Do not create a new vault. Run
01-check.sql: if “Your data” shows vault_config=1 but Keyper cannot see it,
run 02-claim-your-data.sql again with only_username as NULL.
”This database holds N separate vaults”
Section titled “”This database holds N separate vaults””You have more than one legacy username, each with its own vault and its own encryption key. They cannot merge into one account, because their credentials are encrypted with different keys. See Sharing a database.
Keyper still shows the upgrade screen
Section titled “Keyper still shows the upgrade screen”Steps 5 and 6 both need to have run. Run 01-check.sql: “Migration started?”
should say YES and “Access rules” should say the new rules are active.
I ran sql/supabase-setup.sql by mistake
Section titled “I ran sql/supabase-setup.sql by mistake”It stops by itself when it finds existing data, so almost certainly nothing
happened. Run 01-check.sql to confirm, then carry on from step 4.
I have lost my master passphrase
Section titled “I have lost my master passphrase”There is no recovery path, and that was true before this migration too. The vault key is only stored encrypted under that passphrase. If it is genuinely gone, the vault has to be recreated.
Sharing a database with other people
Section titled “Sharing a database with other people”If several people use the same Supabase project, each has their own username and their own vault.
- Create one Supabase account per person (step 3, repeated)
- Run
02-claim-your-data.sqlonce per person, setting both:target_owner UUID := 'their-account-uuid';only_username TEXT := 'their-username'; - Only once every username has an owner, run
03-apply-security.sql - Each person signs in and unlocks with their own master passphrase
03-apply-security.sql refuses to run while anyone is unclaimed, so you cannot
accidentally lock someone out.
After you finish
Section titled “After you finish”Signing in comes first, then your passphrase. Two steps, two secrets.
Switching accounts means signing out and back in. The old in-app user list worked by reading everyone’s rows, which the new rules correctly prevent.
Your master passphrase cannot be reset. You can change it from Settings any time you know the current one. Keep a copy somewhere safe.
If you ran Keyper on a publicly reachable URL: the previous database rules allowed access to vault rows using the public anon key. Now that you have migrated, it is worth refreshing any credentials you stored during that period. If you only ever ran Keyper on localhost, there is nothing extra to do.
Still stuck?
Section titled “Still stuck?”Open an issue at
github.com/pinkpixel-dev/keyper/issues
with the output of 01-check.sql. That one query says exactly where you are.
Do not paste your wrapped_dek, raw_dek or bcrypt_hash values into an issue.
01-check.sql deliberately does not print them.
Credits
Section titled “Credits”Reported privately by Cenk Kurtoglu, who reviewed the setup SQL Keyper ships and got in touch rather than opening a public issue.