Why FaviconAPI?

FaviconAPI started out of a very practical need. While building my own dashboards with Mafl+ (R0GGER/maflplus), I wanted a hassle-free way to fetch favicons and logos and link them to the services on my dashboard - without manually downloading and hosting an image for every single tile.

In practice that turned out to be surprisingly painful. To get decent coverage I always ended up combining multiple sources, and time and again I noticed that the “different” tools I was using were really just reaching for the same underlying providers behind the scenes - mostly Google and DuckDuckGo. When one of those came back with a blank, low-resolution, or generic placeholder icon, I had no fallback and was stuck.

What I was missing was a single tool that treats favicon lookup as a first-class problem: one that knows about many independent sources, queries them together, and intelligently picks the best result instead of betting everything on one upstream. No tool offered that kind of complete, source-aware solution where different providers are connected and complement each other.

So I built it. FaviconAPI brings 10+ favicon providers and four service-icon catalogs together behind one consistent API. It races providers in parallel, normalizes and caches the results, and returns the highest-quality icon it can find - with the others available as explicit fallbacks. It grew from a helper for my own dashboards into a self-hosted favicon proxy that anyone can run.

Full documentation is best read on a desktop computer.

Preload manager

A web interface for the preload database at /admin: domains, icon overrides, the blocklist, popularity stats, CSV import/export and preload runs — everything scripts/manage-preload.js does, without a shell on the server.

The page is off by default. It only exists once you configure a signing secret and a password; until then /admin returns 404, exactly as it did before the feature existed.

Enable it

1. Generate a secret

Run it inside the container, so the key never touches your shell history on the host:

docker compose exec favicon-api npm run admin:secret

That prints a line to paste into .env, the gitignored file docker-compose.yml loads on top of .env.example:

ADMIN_SESSION_SECRET=9f2c…        # 64 random bytes, hex encoded

Always 512 bits (64 bytes), which signs HS512 — the strongest of the two algorithms the server accepts, and there is nothing to gain from a shorter key.

The secret signs the session cookie; it is not the password.

2. Set a password

docker compose exec favicon-api npm run admin:password -- --user yourname

It asks for the password twice without echoing it, then prints the two lines to add to your .env:

ADMIN_USER=yourname
ADMIN_PASSWORD_HASH=scrypt:N=16384,r=8,p=1:…:…

The password itself is never stored — only the scrypt hash, which cannot be turned back into the password. Minimum length is 10 characters; --user defaults to admin. Piping the password in (… | npm run admin:password) works too and skips the confirmation prompt.

Both lines go in the same .env. Then recreate the container with docker compose up -d — env_file is read when the container is created, so a plain restart would keep the old values — and check the result with docker compose exec favicon-api npm run admin:status. Until both the secret and the hash are configured, /admin stays at 404 and the reason is logged at startup.

3. Sign in

Open https://your-host/admin and enter the username and password. Your password manager sees a normal login form, so it can store and fill both fields.

The server hands back a session token in an HttpOnly, SameSite=Strict cookie (Secure as well when you are on https). ADMIN_SESSION_TTL is an idle timeout: every action extends the session, so it only expires after an hour of doing nothing. Tick Keep me signed in to use ADMIN_REMEMBER_TTL (30 days by default) as that window instead.

What you can do

Tab Actions
Domains Search, sort by clicking a column header, and filter by state (on / off). Hide columns you do not need; the choice is remembered. Add a domain, edit usage rank and icon override inline, enable or disable, delete. Rank is rolling request count — a worldwide list position cannot outrank real traffic. Tick rows (or the header checkbox for the whole page) to disable, enable or delete several at once.
Blocklist Add exact (example.com) or wildcard (*.example.com) patterns, seed the built-in service/infra set, unblock.
Stats Domain, override and blocklist counts, the active configuration, hits per month and a Recalculate ranks button.
Import / Export Download the CSV (all or enabled only), paste or upload one to import. Same column order as the CLI.
Preload run Preview the ranked domain set in a modal (Most popular / Most visited / Highest keyword rank), start preload-top-sites.js against this instance, follow its log live, stop it, and generate a host crontab line from the same options.

Edits made here behave exactly like CLI edits: adding, enabling, disabling or changing a domain marks the row as manual, so no automated pass (hit counting, preload writeback) flips it back.

Preload runs

A run is a separate process, so it keeps going when you close the tab, and its progress is visible from every cluster worker. A lock file next to the database prevents two runs at once; if the process disappears mid-run the state is reported as interrupted and the lock is cleared automatically.

Dry run lists the selected domains without calling the API — the quickest way to check what a set of filters would actually warm. View list opens a modal with a ranked table (domain and Adult columns, no icons).

Timeout is the per-request budget passed to the script as --timeout, in milliseconds. The default of 30000 is enough for most domains, but slow sites need more: raise it to 60000 for a scheduled run. A domain that runs out of budget shows up in the log as This operation was aborted and the run continues with the next one.

Schedule with cron turns the current form into a crontab line for the host (docker exec or docker compose exec -T). It tracks source, adult include/exclude, limit, min rank, concurrency, timeout and skip flags; dry run is omitted so a scheduled job actually warms the cache. Optional Recalculate ranks first prepends manage-preload.js recalc when the source is the preload database. Paste the line with crontab -e. Cron's PATH is minimal, so the generator uses the full docker binary path. See Performance §10 for the recommended Sunday 03:00 schedule.

The run form can pull three worldwide sets: most popular (Backlinko / Semrush, top 100, NSFW already removed), most visited (SimilarWeb, public top 50, remaining slots filled from Backlinko) and highest keyword rank (DataForSEO). Adult / porn domains is include or exclude — SimilarWeb's Adult-category sites are dropped when you pick exclude.

A live run from one of those sets stores the domains in the list with that source (backlinko, similarweb, dataforseo) and a 1-based list position (#1 on the Source column). Usage rank is not overwritten. Dry run does not write.

Configuration

Variable Default Meaning
ADMIN_SESSION_SECRET (unset) Signing key for the session cookie. Unset, or shorter than 32 bytes, keeps /admin at 404. Legacy name ADMIN_JWT_SECRET still works, with a deprecation warning.
ADMIN_PASSWORD_HASH (unset) scrypt hash of the login password. Unset or malformed keeps /admin at 404.
ADMIN_USER admin Login name.
ADMIN_SESSION_TTL 3600 Idle timeout in seconds of a normal session.
ADMIN_REMEMBER_TTL 30d Idle timeout when Keep me signed in was ticked. Takes 30d, 12h, 45m or seconds.
ADMIN_LOGIN_MAX_ATTEMPTS 10 Failed sign-ins per IP per 15 minutes before further attempts are refused.

Security notes

Icon overrides take a moment to apply

Each worker re-reads the override table when the database file changes, at most once per PRELOAD_OVERRIDE_RELOAD_MS (default 30s). A new or changed icon override is live within that window; the blocklist applies immediately in the worker that made the change and within the same window elsewhere.

✓ URL copied