BSwiper: mass-delete Bluesky posts, reposts and likes from your self-hosted PDS (Ubuntu 22.04 guide)

Running your own Bluesky Personal Data Server (PDS) gives you full control over your data. But what if you want to automatically delete posts or reposts older than a few days?
This guide shows you how to install a Node.js cleanup script on Ubuntu 22.04 that will connect to your Bluesky account and delete any content older than a set number of days — directly from your own PDS. This can be run from any Ubuntu installation or on your PDS directly, whatever is easier for you.
✅ What this script can do
- 🔥 Delete posts older than
X
days - 🔁 Optionally delete reposts
- ⚙️ Fully configurable from the script itself
- 🌐 Works with self-hosted PDS (no reliance on bsky.social) but can work on non-self-hosted bsky.social accounts
- 🖥️ Runs securely on your own server
- ⚠️ IMPORTANT: this script is DESTRUCTIVE! Be sure to backup your PDS before running it to revert any undesired deletions!
📦 Step 1: Install Node.js (v18+)
Ubuntu 22.04 includes an older Node.js version by default. We’ll install the latest LTS version manually:
sudo apt update
sudo apt install curl -y
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
Verify the install:
node -v
# Should output something like v18.x.x
📁 Step 2: Set up the script directory
Create a working folder:
mkdir ~/bswiper
cd ~/bswiper
Initialize a basic Node project:
npm install @atproto/api
Install the Bluesky SDK:
npm install @atproto/api
📝 Step 3: Create the wiping script
Create a file called bswiper.js
:
nano bswiper.js
Paste in the following:
const { BskyAgent } = require('@atproto/api');
const handle = 'your-handle.bsky.social'; // Replace with your handle or DID. Do not include the @ symbol
const password = 'your-app-password'; // Replace with your password
const agent = new BskyAgent({ service: 'https://your.pds.domain' });
// ==== ✅ CONFIG OPTIONS ====
const DELETE_POSTS = true;
const DELETE_REPOSTS = true;
const DELETE_LIKES = true;
const DAYS_TO_KEEP = 2;
// ============================
async function deleteRecords({ collection, label }) {
console.log(`\n📦 Processing ${label}s from ${collection}...`);
const now = new Date();
const cutoff = new Date(now.getTime() - DAYS_TO_KEEP * 24 * 60 * 60 * 1000);
let cursor = undefined;
let deletedCount = 0;
while (true) {
const res = await agent.api.com.atproto.repo.listRecords({
repo: handle,
collection,
limit: 100,
cursor,
});
if (!res.data.records.length) break;
for (const record of res.data.records) {
const { uri, value } = record;
const rkey = record.rkey || uri.split('/').pop();
const createdAt = new Date(value.createdAt);
if (createdAt < cutoff) {
try {
await agent.api.com.atproto.repo.deleteRecord({
repo: handle,
collection,
rkey,
});
console.log(`🗑️ Deleted ${label}: ${uri} (Created at ${createdAt.toISOString()})`);
deletedCount++;
} catch (err) {
console.error(`❌ Failed to delete ${label} ${uri}:`, err.message);
}
} else {
console.log(`⏳ Keeping ${label}: ${uri} (Created at ${createdAt.toISOString()})`);
}
}
if (!res.data.cursor) break;
cursor = res.data.cursor;
}
console.log(`✅ Done. Deleted ${deletedCount} ${label}(s) older than ${DAYS_TO_KEEP} days.`);
}
async function main() {
await agent.login({ identifier: handle, password });
if (DELETE_POSTS) {
await deleteRecords({
collection: 'app.bsky.feed.post',
label: 'post',
});
}
if (DELETE_REPOSTS) {
await deleteRecords({
collection: 'app.bsky.feed.repost',
label: 'repost',
});
}
if (DELETE_LIKES) {
await deleteRecords({
collection: 'app.bsky.feed.like',
label: 'like',
});
}
console.log('\n🎉 Cleanup complete.');
}
main();
✅ Replace:
your-handle.bsky.social
with your Bluesky handle or DID. Do not include the @ symbolyour-app-password
with your Bluesky account passwordhttps://your-pds.domain
with your PDS URL (e.g.,https://bsky.yourdomain.com
) or for bsky.social hosted accounts usehttps://bsky.social
✅ Set config options:
DELETE_POSTS
= true/false to enable/disable deletion of your postsDELETE_REPOSTS
= true/false to enable/disable deletion of your repostsDELETE_LIKES
= true/false to enable/disable deletion of likesDAYS_TO-KEEP
= define age of posts in days to preserve. For example,2
would mean all posts and reposts older than 2 days would be deleted
Save and exit (CTRL+O
, Enter
, CTRL+X
).
🚀 Step 4: Run the Script
Run the cleanup manually:
node bswiper.js
It will:
- Log into your Bluesky account
- Fetch all posts and reposts from your repo
- Delete any records older than 2 days (or whatever you set)
🕑 Optional: Run it daily with cron
Open your crontab:
crontab -e
Add this line to run the script every day at 3 AM:
0 3 * * * /usr/bin/node /home/youruser/bswiper/bswiper.js >> /home/youruser/bswiper/cron.log 2>&1
Be sure to update the location to match your system i.e. /home/youruser/... should be set to your home directory name etc