Created by @alanshurafa
Reviewed and merged by the Open Brain maintainer team — thank you for building the future of AI memory!
Backfill content fingerprints on existing thoughts and safely remove duplicates discovered during the process.
If you imported thoughts before the Content Fingerprint Dedup primitive was in place, those rows will have a NULL content_fingerprint. This recipe computes fingerprints for all existing rows and then identifies and removes duplicates — rows whose content already exists in the table under a properly fingerprinted copy.
Two scripts work together:
-
backfill-fingerprints.mjs— Scans all NULL-fingerprint rows and patches each one with a computed SHA-256 fingerprint. Resumable via state file. -
delete-duplicates.mjs— Finds NULL-fingerprint rows whose content already has a fingerprinted copy in the table. Defaults to report-only mode (no deletions). Pass--deleteto actually remove duplicates.
- Working Open Brain setup (guide)
- Content Fingerprint Dedup primitive applied (so
content_fingerprintcolumn exists) - Node.js 18+
Copy this block into a text editor and fill it in as you go.
FINGERPRINT DEDUP BACKFILL -- CREDENTIAL TRACKER
--------------------------------------
FROM YOUR OPEN BRAIN SETUP
Supabase URL: ____________
Service role key: ____________
--------------------------------------
1. Clone or download this recipe
Copy the recipe folder to your local machine.
2. Configure credentials
Copy .env.example to .env and fill in your Supabase credentials:
cp .env.example .env
# Edit .env with your Supabase URL and service role key3. Run the backfill
This computes and patches fingerprints for all rows where content_fingerprint is NULL:
node backfill-fingerprints.mjsThe script processes rows in batches of 1000, saving progress to backfill-state.json after each batch. If interrupted, it resumes from where it left off.
4. Generate a duplicate report
Before deleting anything, see what would be removed:
node delete-duplicates.mjs --report-onlyThis scans remaining NULL-fingerprint rows, computes their fingerprints, and reports how many are duplicates of existing fingerprinted rows — without deleting anything.
5. Remove duplicates (when ready)
Once you've reviewed the report and are satisfied:
node delete-duplicates.mjs --deleteCaution
The --delete flag permanently removes rows. Make sure you've reviewed the report first. The script also backfills fingerprints on any genuine orphan rows (those with no existing duplicate).
After running both scripts:
- Every row in the
thoughtstable has a non-NULLcontent_fingerprint - No duplicate content exists (each unique fingerprint appears once)
- The
content_fingerprintunique constraint is now fully enforceable
Verify with:
-- Count remaining NULL fingerprints (should be 0)
select count(*) from thoughts where content_fingerprint is null;
-- Check for duplicate fingerprints (should return 0 rows)
select content_fingerprint, count(*) as copies
from thoughts
where content_fingerprint is not null
group by content_fingerprint
having count(*) > 1
limit 10;The normalization matches the Content Fingerprint Dedup primitive exactly:
- Trim whitespace and collapse runs of whitespace to single spaces
- Lowercase
- Strip trailing punctuation (
.!?;:,) - Strip possessives (
'sand\u2019s) - Strip trailing
sfrom the last word if the word has 4+ characters - SHA-256 hex digest of the result
This means "The dog's toys." and "the dogs toy" produce the same fingerprint.
Issue: Script reports many "duplicate" PATCH errors (409 / 23505) Solution: This means the computed fingerprint already exists on another row. The backfill script counts these but skips them — this is expected behavior. Run the cleanup script afterward to remove the duplicates.
Issue: Script hangs or times out on large tables Solution: The scripts use cursor-based pagination and save state after each batch. If a request times out, the script retries after 5 seconds. For very large tables (100K+), expect the backfill to take 10-30 minutes.
Issue: content_fingerprint column doesn't exist
Solution: Apply the Content Fingerprint Dedup primitive first. The column must exist before running these scripts.
Issue: Want to reset and start over
Solution: Delete the state file (backfill-state.json or cleanup-state.json) and run the script again. It will start from the beginning.