True union
A domain counts as active when it is active in any profile. Everything else is added switched-off instead of being dropped.
Add a domain to one profile's allowlist or denylist and every other profile gets it on the next run. Allowlist + denylist only, add-only, no duplicates — powered by a GitHub Action and configured entirely with GitHub Variables.
Free · open source · GPL-3.0
The tool is deliberately narrow: two lists, one direction, no deletions. That is what makes an unattended sync trustworthy.
A domain counts as active when it is active in any profile. Everything else is added switched-off instead of being dropped.
Nothing is ever deleted and no toggle is flipped off. Even a bulk write resends the profile's existing entries.
sync-state.json remembers what was synced where, so the same domain is never written twice.
Delete a domain by hand and it stays deleted — the state DB treats its absence as "the user removed it".
20+ missing items go out in a single PUT: 125 domains take ~2 seconds instead of 21 minutes. Small batches get a 10 s cooldown.
Keys, profile ids and the bot token live in GitHub Variables. That is why this repository can be public.
One run is a straight line: read everything, build the union, fill in the gaps, remember what happened.
GitHub Actions runs twice a day, or on demand from the Actions tab with dry_run / full inputs.
Both lists are fetched from every configured profile, following pagination cursors, and lowercased / de-duplicated.
Domains are combined per list. A domain in the allowlist never leaks into the denylist (and vice versa) — a test enforces it.
For each profile: todo = union − has − state. Anything the state DB already knows about is skipped, which is how manual deletes survive.
Big batches are a single verified PUT; small batches are individual POSTs with a cooldown. Failures fall back to POST one by one.
The state DB is committed back to the repo and one Telegram message summarises what happened — including a crash, if there was one.
Synced
Never touched
Syncing settings between profiles causes surprises (one wants logging, another must not). Restricting to two domain lists is what makes an unattended run safe.
Seven steps. Nothing is installed locally and there are no secrets to paste into code.
Public if you are publishing the template, private for your own copy (the state DB holds your domains).
Settings → Secrets and variables → Actions → Variables
Use the Variables tab, not Secrets — that is what the workflow reads.
GitHub only reads workflows from .github/workflows/ — the folder name starts with a dot, which many tools hide. See the install guide →
Settings → Actions → General → Workflow permissions → Read and write.
Without it sync-state.json is never committed and every run re-adds the same domains.
Actions → NextDNS profile union → Run workflow with dry_run = true. Nothing is written; you only see what would happen.
Repeat with dry_run = false. Done — it now runs itself every 12 hours.
Everything is configured in the repository settings — the code contains no credentials.
| Variable | Required | Example | Purpose |
|---|---|---|---|
| NEXTDNS_API_KEY1…20 | yes | 0123456789abcdef… | API key that can read profile N |
| NEXTDNS_PROFILE_ID1…20 | yes | ab12cd | The profile id |
| NEXTDNS_LABEL1…20 | no | Home | Friendly name in reports |
| TG_TOKEN | no | 1234567890:AAH… | Telegram bot token |
| TG_CHAT | no | 123456789 | Your Telegram user id |
| TG_ON | no | true | Toggle notifications |
The defaults are fine — you never have to set these.
Numbers may skip: 1, 2, 7 works.
Optional — Actions does everything. Handy for checking variables or debugging.
--full re-adds domains you deleted on purpose. Preview it with --dry-run --full first.
The short answers. The full 32-question version is in the repo.
Yes — no credentials are committed. But sync-state.json lists your domains, so make your own fork private.
No — deliberately. Only the allowlist and the denylist are touched.
No. This is a union, not a mirror — nothing is ever deleted. Your manual deletes are remembered and never re-added.
Only to bring back a domain you deleted on purpose, or after the state DB broke. Preview with dry_run = true + full = true first.
Each POST waits 10 seconds so NextDNS never rate-limits you. Batches of 20+ use one bulk PUT and finish in seconds.
No. The tool talks to the NextDNS REST API only — standard library Python, nothing to install.
Fourteen focused pages instead of one giant one — so you can find the exact thing you need.
A question, a bug, or a profile setup that refuses to work — send it here. The form is protected by Cloudflare Turnstile, so no account and no email address is required.
Protected by Cloudflare Turnstile. The token is verified server-side by a Cloudflare Pages Function; the secret key never reaches the browser.