docs
v1.1.0 GitHub
Introduction

Feather docs

A whisper-light Chrome extension that watches Discord for new messages in real time — firing webhook alerts, logging everything to Supabase, and showing a clean overlay indicator right on the Discord page.

👁️
Live DetectionMutationObserver watches Discord's DOM. No polling, no API. New messages caught the instant they appear.
🔔
Webhook AlertsPosts to a Discord webhook with author, channel, message preview, and running count every time.
🗄️
Supabase LoggingEvery detected message is inserted as a row with full metadata. Permanent, queryable, yours.
🛡️
Smart SuppressionTwo-layer system blocks false triggers on channel switch — URL polling + burst detection.
🧹
DeduplicationTracks message IDs so Discord's optimistic rendering never double-counts your own messages.
📊
Popup DashboardThree-tab UI with live stats, scrollable log, JSON export, and full reset.

How the pieces connect

Three scripts run independently and communicate via Chrome's message passing API:

ScriptRuns inResponsibility
content.jsDiscord tabWatches DOM, detects messages, shows overlay, extracts metadata
background.jsService workerReceives events, fires webhook, writes to Supabase, relays popup commands
popup.jsToolbar popupDisplays stats + log, sends toggle/reset commands, handles export

File structure

feather/ ├── manifest.json — Chrome MV3 manifest, permissions, host rules ├── background.js — Service worker: webhook + Supabase + tab relay ├── content.js — Injected into Discord: observer + overlay ├── popup.html — Popup markup and all inline styles ├── popup.js — Popup logic: tabs, storage, live updates ├── styles.css — Overlay indicator styles injected into Discord └── icons/ ├── icon16.png ├── icon48.png └── icon128.png
Getting Started

Installation

Feather is a private unpacked extension — it never goes through the Chrome Web Store. You load it directly from your files. Works the same in Chrome, Edge, Brave, and Arc.

ℹ️
Before you startYou need all six files plus the icons/ folder in a single directory. Configure your credentials in background.js before loading — easier than reloading twice.
1
Put all files in one folder
Create a folder somewhere permanent — e.g. C:\Users\you\feather\. Place all six files and the icons\ subfolder inside it. Don't move the folder after loading or the extension will break.
Files in folder
2
Add your credentials to background.js
Open background.js in any text editor. Fill in the three constants at the top — your webhook URL, Supabase URL, and Supabase anon key. Save the file. See Configuration for details.
3
Open Chrome Extensions
In Chrome's address bar type chrome://extensions and press Enter.
Chrome extensions page
4
Enable Developer mode
In the top-right corner of the extensions page, toggle Developer mode on. The page will refresh and show new buttons.
Developer mode toggle
5
Click "Load unpacked"
Click the Load unpacked button that appeared. Navigate to your feather\ folder and click Select Folder.
Load unpacked button
6
Pin the extension
Click the 🧩 puzzle-piece icon in the Chrome toolbar and pin Feather so you can always reach the popup.
Pin extension
7
Open Discord and verify
Navigate to discord.com/app. Within a couple of seconds the Feather overlay indicator should appear in the bottom-right corner showing watching.
Feather overlay on Discord

Reloading after edits

Every time you change any file: go to chrome://extensions, click the ↺ refresh icon on the Feather card, then reload the Discord tab. Chrome does not auto-reload unpacked extensions.

1
Put all files in one folder
Create a permanent folder — e.g. ~/Documents/feather/. Place all six files and the icons/ subfolder inside. Don't move it after loading.
2
Add your credentials to background.js
Open background.js in TextEdit, VS Code, or any text editor. Fill in the three constants at the top. See Configuration.
3
Open Chrome Extensions
Go to chrome://extensions in Chrome's address bar.
4
Enable Developer mode
Toggle Developer mode on in the top-right corner of the page.
5
Click "Load unpacked"
Click Load unpacked. In the Finder dialog navigate to ~/Documents/feather/ and click Open.
6
Pin and verify
Pin Feather from the 🧩 menu. Open discord.com/app — the overlay should appear bottom-right within a few seconds.
💡
Quick reload shortcutOn macOS hit ⌘ R on the Discord tab after reloading the extension.
1
Put all files in one directory
Pick a permanent path — e.g. ~/.local/share/feather/.
mkdir -p ~/.local/share/feather
cp -r /path/to/feather/* ~/.local/share/feather/
2
Add your credentials
nano ~/.local/share/feather/background.js
3
Open Chrome Extensions
Launch Chrome or Chromium and go to chrome://extensions.
4
Enable Developer mode + Load unpacked
Toggle Developer mode, click Load unpacked, navigate to ~/.local/share/feather/, click Open.
5
Open Discord and verify
Go to discord.com/app. The overlay should appear in the bottom-right corner.
⚠️
Snap Chrome on LinuxIf Chrome was installed via Snap it sandboxes filesystem access. Move the extension to ~/snap/chromium/current/feather/ or install Chrome via the official .deb package instead.
Setup

Configuration

Everything lives in four constants at the very top of background.js. Open the file, fill them in, save, reload the extension. That's it.

// ── background.js — top of file ──────────────────────────

const DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN";
const SUPABASE_URL        = "https://your-project-id.supabase.co";
const SUPABASE_ANON_KEY   = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
const SUPABASE_TABLE      = "feather_logs"; // change if you renamed your table
⚠️
Reload required after every editGo to chrome://extensions, click the icon on the Feather card, then reload your Discord tab.

Both integrations are fully optional. Leave a value as the placeholder string and that integration is silently skipped — no errors. The popup's Overview tab shows live status dots so you can see what's configured.

Chrome storage keys

Feather stores runtime state in chrome.storage.local. You never manage these directly, but it's useful to know they exist:

feather_enabledboolean
Whether detection is active. Defaults to true on first run.
feather_countnumber
Running session message count. Persists until you clear it.
feather_discord_openboolean
Whether a Discord tab with the content script is currently active.
feather_recentarray
Last 50 detected messages with full metadata. Shown in the Log tab.
feather_integrationsobject
{ webhook: boolean, supabase: boolean } — written by background.js on startup.

Wipe all of this cleanly using the Full reset button in the popup Settings tab.

manifest.json — why host_permissions matter

Chrome MV3 requires outbound fetch() targets to be explicitly listed in host_permissions. Without this, requests to Supabase and Discord are silently blocked from the service worker.

"host_permissions": [
  "https://discord.com/*",
  "https://*.supabase.co/*"
]

Both are already in the current manifest.json. Don't remove them.

Integration

Webhook

On every detected message, Feather fires a POST to a Discord webhook URL with the author, channel, a message preview, and the running count.

Getting a webhook URL

1
Open a server you manage
You need Manage Webhooks permission on the target channel. It can be any server — a private logging server works great.
2
Open channel settings
Click the ⚙️ gear icon next to the channel name → IntegrationsWebhooksNew Webhook. Name it "Feather".
Create webhook in Discord
3
Copy the Webhook URL
Click Copy Webhook URL. It looks like:
https://discord.com/api/webhooks/1234567890/abc123xyz...
4
Paste into background.js
Set DISCORD_WEBHOOK_URL to this value and reload the extension.

What the webhook message looks like

# feather-logs
Discord
feather webhook Today at 14:32
🔔 New Discord Message · 14:32:07
Author: username123
Channel: general
Message: hey has anyone seen the new update yet
Total count: 7

Message content is capped at 200 characters. For full content, use Supabase instead.

Testing your webhook

curl -X POST YOUR_WEBHOOK_URL \
  -H "Content-Type: application/json" \
  -d '{"content": "Feather test 🪶"}'

If your logging channel gets a message, the URL is correct.

Integration

Supabase

Every detected message is inserted as a row into your Supabase database — a permanent, queryable log of everything Feather has ever seen.

⚠️
manifest.json must list Supabase in host_permissionsChrome MV3 silently blocks outbound fetches to unlisted hosts. The current manifest.json already includes https://*.supabase.co/* — do not remove it.

Step 1 — Create a project

Go to supabase.com, sign in, click New project. Give it a name, set a database password, pick a region. Free tier is more than enough — 500MB and unlimited API calls.

Step 2 — Create the table

In your project sidebar, click SQL Editor. Paste this block and click Run:

create table feather_logs (
  id         bigint generated always as identity primary key,
  author     text,
  content    text,
  channel    text,
  count      integer,
  timestamp  timestamptz default now()
);

-- Enable row-level security (required to use the anon key)
alter table feather_logs enable row level security;

-- Allow the anon key to INSERT rows
create policy "Allow anon insert"
  on feather_logs for insert to anon
  with check (true);

-- Allow the anon key to SELECT rows
create policy "Allow anon select"
  on feather_logs for select to anon
  using (true);
ℹ️
Why the RLS policies?Supabase enables Row Level Security by default. Without the insert policy, the anon key is denied permission to write rows — you'd get a silent 403.

Step 3 — Get your credentials

In your project sidebar go to Project Settings → API:

FieldWhereLooks like
Project URLSettings → API → Project URLhttps://abcdefgh.supabase.co
anon / public keySettings → API → Project API keyseyJhbGci... (long JWT)

Step 4 — Paste into background.js

const SUPABASE_URL      = "https://abcdefgh.supabase.co";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
const SUPABASE_TABLE    = "feather_logs";

Step 5 — Verify it's working

Trigger a detection, then go to Table Editor → feather_logs in Supabase. A new row should appear within one second.

Useful queries

-- All messages, newest first
select timestamp, author, channel, content
from feather_logs
order by timestamp desc;

-- Messages per author (leaderboard)
select author, count(*) as total
from feather_logs
group by author
order by total desc;

-- Last 24 hours only
select * from feather_logs
where timestamp > now() - interval '24 hours'
order by timestamp desc;

-- Search by author
select * from feather_logs
where author ilike '%username%'
order by timestamp desc;
Reference

Detection Logic

How Feather identifies real messages, suppresses false triggers, deduplicates, and extracts author and channel metadata — all without touching the Discord API.

Identifying a message node

Feather runs a MutationObserver on document.body with { childList: true, subtree: true }. Every added node is checked against three criteria:

1
data-list-item-id starts with "chat-messages-"
Discord stamps every message <li> with this attribute. Primary signal, most reliable.
2
Contains a child with that attribute
Covers wrapper divs Discord occasionally adds around the <li> in certain UI states.
3
Contains a messageContent_ or cozyMessage class
Class-based fallback for older Discord versions or edge cases.

Channel switch suppression

When you switch channels, Discord bulk-inserts ~50 historical messages. Feather uses two independent layers:

LayerMechanismCooldown
URL pollerChecks location.href every 150ms. Any URL change immediately arms a cooldown.2000ms
Burst detectorCounts message nodes per callback. 3+ nodes in a single callback = suppressed + cooldown re-armed.2000ms

If you're in an extremely active channel you can increase BURST_LIMIT in content.js:

// content.js — near the top
const BURST_LIMIT  = 5;    // raise if false suppressions happen
const NAV_COOLDOWN = 2000; // ms to ignore after URL change

Deduplication

Discord uses optimistic rendering — your message appears instantly, then gets replaced with the server-confirmed version. Feather maintains a Set of seen data-list-item-id values, capped at 500 entries, and skips data-is-local-message="true" nodes entirely.

Author extraction

Discord groups consecutive messages — only the first shows a username header. Feather handles this by first looking for [class*="username_"] within the node, then walking backwards through previous messages to find the most recent username header above it, falling back to "unknown".

Channel name extraction

document.querySelector('h3[class*="title_"]')
  || document.querySelector('[class*="channelName_"]')
  || document.querySelector('h1[class*="title_"]')

Each tab tracks its own channel independently.

Reference

Shortcuts

One keyboard shortcut. Works anywhere on discord.com as long as the Discord tab is focused.

Toggle detection on / off
Instantly pauses or resumes the MutationObserver. State is saved to storage and persists across reloads.
AltQ
More shortcuts can be added in content.js — look for the keydown event listener near the bottom of the file.
💡
Overlay reflects state immediatelyWhen you press Alt Q, the overlay switches between watching and paused with a smooth transition. No need to open the popup to confirm.
Help

Troubleshooting

Most issues come down to one of five things. Work through each checklist below.

Overlay doesn't appear on Discord

🔴
Steps to fix 1. Hard reload Discord: Ctrl Shift R (Windows/Linux) or ⌘ Shift R (Mac).
2. Check that Feather is enabled at chrome://extensions.
3. Open Discord's DevTools (F12) → Console → look for Feather errors.
4. Make sure you're on discord.com not the desktop app.

Popup toggle / reset does nothing

🔴
Steps to fix 1. Go to chrome://extensions → Feather → click the service worker link.
2. Check Console for errors. A missing discordTabs variable is the usual cause.
3. Click ↺ to reload the extension, then reload the Discord tab.
4. Check the Discord tab's DevTools Console for message-passing errors.

Webhook messages not arriving

🔴
ChecklistDISCORD_WEBHOOK_URL is set (not the placeholder).
manifest.json includes "https://discord.com/*" in host_permissions.
☐ Background DevTools shows no [Feather] Webhook failed: error.
☐ Test the URL directly with curl (see the Webhook page).

Supabase rows not appearing

🔴
ChecklistSUPABASE_URL and SUPABASE_ANON_KEY are correct (no trailing slashes).
manifest.json includes "https://*.supabase.co/*" in host_permissions.
☐ The feather_logs table exists — re-run the SQL from the Supabase page.
☐ Both RLS policies exist.
☐ Background DevTools shows no [Feather] Supabase log failed: error.

Channel switches still trigger false detections

⚠️
Increase the burst limit or cooldown Open content.js and increase BURST_LIMIT from 3 to 5, or increase NAV_COOLDOWN from 2000 to 3000. Reload the extension and Discord.

Messages being double-counted

⚠️
Check for duplicate extension loads Go to chrome://extensions — if Feather appears twice, you've loaded it from two different folders. Remove the duplicate.

Opening the background service worker DevTools

1
Go to chrome://extensions
Find the Feather card.
2
Click "service worker"
Click the blue service worker link under the extension name. This opens a dedicated DevTools window for the background script.
3
Check the Console tab
All [Feather] prefixed logs and errors appear here.