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

BSwiper: mass-delete Bluesky posts, reposts and likes from your self-hosted PDS (Ubuntu 22.04 guide)
BS for all the bullshit we post on Bluesky 😂

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 @ symbol
  • your-app-password with your Bluesky account password
  • https://your-pds.domain with your PDS URL (e.g., https://bsky.yourdomain.com) or for bsky.social hosted accounts use https://bsky.social

✅ Set config options:

  • DELETE_POSTS = true/false to enable/disable deletion of your posts
  • DELETE_REPOSTS = true/false to enable/disable deletion of your reposts
  • DELETE_LIKES = true/false to enable/disable deletion of likes
  • DAYS_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