Skip to content

tribett/media-steward

Repository files navigation

The Media Steward

Self-hosted family media stewardship — DNS blocking, curated RSS, and intentional media rhythms.

One command. Covers every device on your WiFi.


License Next.js Docker TypeScript Raspberry Pi Extension


What is this?

The Media Steward is a self-hosted household tool that runs on any old computer and gives your family four things:

  • 🛡️ Fence — DNS-level content blocking. Configure presets (Light / Balanced / Intentional), toggle blocklist sources, and schedule time blocks when all internet is cut off. Because it runs at the DNS layer, it covers every device on your WiFi automatically — phones, tablets, smart TVs, everything — with a single router setting change.

  • 🧩 Extension — A Chrome/Firefox extension that surgically fixes YouTube: redirects the algorithmic homepage to your subscriptions feed, removes Shorts everywhere, and hides the "Up Next" recommendations sidebar. What DNS can't reach (inside a platform), the extension handles.

  • 🌱 Cultivate — A curated RSS feed reader. Add only the sources you've chosen. No algorithmic feeds, no recommendations, no infinite scroll. Just your articles, in order.

  • 📖 Steward — Track your household's intentional media rhythms — practices you've committed to, like "no screens before breakfast" or "Friday family movie at 7 PM". Shown on the dashboard each day.

The interface uses a warm amber aesthetic designed to feel calm and purposeful, not addictive.


Quick Start

Requirements: Docker, macOS or Linux (including Raspberry Pi 4/5).

git clone https://github.com/tribett/media-steward.git
cd media-steward
./start.sh

Then open http://localhost:3000 and complete the 4-step setup wizard.

Finally, point your router's Primary DNS to this machine's IP address. Every device on your network is now covered.

BrightSpeed routers: Router Settings → Internet → DNS Settings → Primary DNS


Raspberry Pi

Media Steward runs great on a Raspberry Pi 4 or 5 (64-bit Raspberry Pi OS). The node:22-alpine Docker image is multi-arch and pulls the right arm64 layer automatically.

One gotcha: Raspberry Pi OS (and Ubuntu) bind a stub resolver to port 53 via systemd-resolved. You need to free that port before starting:

sudo systemctl disable --now systemd-resolved
sudo rm -f /etc/resolv.conf
echo 'nameserver 8.8.8.8' | sudo tee /etc/resolv.conf

Then run ./start.sh as normal. The script will warn you automatically if port 53 is still occupied.

Install Docker on Pi:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER   # log out and back in after this

Browser Extension

The extension lives in apps/extension/ and requires no build step.

Install (local):

  1. Chrome: chrome://extensions → Enable Developer modeLoad unpacked → select apps/extension/
  2. Firefox: about:debuggingLoad Temporary Add-on → select apps/extension/manifest.json

What it does:

Toggle Effect
Subscriptions as home Redirects youtube.com to your subscriptions feed
Remove Shorts Hides Shorts shelves in feed, search, and channels
Hide recommendations Removes the "Up Next" sidebar on watch pages
Hide trending Removes Trending & Explore from navigation

Settings sync across your devices via chrome.storage.sync. The extension makes zero network requests and collects no data.

Publishing to Chrome Web Store: The icons are already generated as PNG at all required sizes. You need a $5 Chrome Developer account, then upload the apps/extension/ folder as a zip. See apps/extension/README.md for full instructions.


Architecture

media-steward/
├── apps/
│   ├── dns/           # UDP + TCP DNS server (dns2, port 53)
│   ├── web/           # Next.js 15 App Router dashboard (port 3000)
│   └── extension/     # Chrome/Firefox browser extension (Manifest V3)
├── packages/
│   ├── db/            # Prisma + SQLite schema & client
│   ├── blocklists/    # Steven Black host-file blocklist loader
│   └── types/         # Shared TypeScript types
├── docker-compose.yml
└── start.sh

The system has two complementary layers:

Layer Tool What it blocks
Network (all devices) DNS server + dashboard Ad networks, TikTok, social media, custom blocklists
Browser (per device) Chrome/Firefox extension YouTube Shorts, algorithmic homepage, sidebar recommendations

DNS blocks whole domains — fast, covers every device on WiFi. The browser extension goes inside YouTube and surgically removes the addictive parts while leaving subscriptions intact.

Stack:

  • Next.js 15 — App Router, Server Actions, Server Components
  • Prisma 6 + SQLite — zero-config embedded database
  • dns2 — Node.js DNS server (UDP + TCP)
  • Tailwind v4 + shadcn/ui — styling
  • Turborepo — monorepo build orchestration
  • Docker Compose — one-command deployment
  • Manifest V3 — Chrome/Firefox extension, no build step

How DNS blocking works:

Device DNS query → DNS server (port 53)
                 ↓
         Check fence_enabled (DB cache, 30s TTL)
                 ↓
         Check schedule blocks (is this a blocked hour?)
                 ↓
         Check domain against blocklist (6h cache)
                 ↓
         NXDOMAIN (blocked) or upstream forward (allowed)

The DNS server caches the blocklist in memory (refreshed every 6 hours) and deduplicates concurrent refresh requests. Cold queries add ~1ms latency; cached queries are negligible.


Fence Presets

Preset What's blocked
Light Ads & tracking domains
Balanced + Algorithmic feeds (YouTube home, TikTok, Reels, Twitter/X feed)
Intentional + Social media entirely (Facebook, Instagram, Reddit, etc.)

Each preset is powered by Steven Black's unified hosts list with category filters applied.


Schedule Blocks

Add time blocks when all internet access is cut off regardless of fence preset. Useful for:

  • Bedtime (e.g., Mon–Fri 9 PM – 7 AM)
  • Dinner (e.g., every day 6 PM – 7 PM)
  • School hours (e.g., weekdays 8 AM – 3 PM)

Configured per-day with AM/PM time selectors.


Development

# Install dependencies
npm install

# Run database migrations
npm run db:push --workspace=packages/db

# Seed example data
npm run db:seed --workspace=packages/db

# Start the web app (dev server)
cd apps/web && DATABASE_URL=file:../../dev.db npm run dev

# Start the DNS server (separate terminal)
cd apps/dns && DATABASE_URL=file:../../dev.db npm run dev

# Run all tests
npm test

Testing:

  • DNS logic: Vitest unit tests (apps/dns/src/__tests__/)
  • Blocklists: Vitest unit tests (packages/blocklists/)
  • E2E: Playwright (apps/web/e2e/)
npm test                    # all tests
npm run test --workspace=apps/dns   # DNS only

Configuration

Environment variable Default Description
DATABASE_URL file:/app/data/dev.db SQLite database path
UPSTREAM_DNS 8.8.8.8 Upstream DNS server for non-blocked queries

Set in .env (copy from .env.example) for local dev, or via Docker Compose environment block for production.


Router Setup

After running ./start.sh, find your machine's IP (printed by the script) and set it as your router's primary DNS:

  • BrightSpeed: Router Settings → Internet → DNS Settings → Primary DNS
  • Most routers: Advanced → DNS → Primary DNS Server

Your secondary DNS can remain your ISP's default (e.g., 8.8.4.4) for fallback. The media steward machine must stay on and connected to your network.


Why self-hosted?

  • No subscription — runs on hardware you already own (old laptop, Raspberry Pi, NUC)
  • No telemetry — your DNS queries never leave your home network
  • No app to install on every device — one DNS change covers everything
  • Fully auditable — read the source, modify it, make it yours

License

MIT — do whatever you want with it.


Built for intentional households. ◈

About

Self-hosted family media stewardship — DNS blocking, curated RSS, and intentional media rhythms. One command covers every device on your WiFi.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors