<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Artemy Andruschak</title>
    <description>The latest articles on DEV Community by Artemy Andruschak (@artemy_and).</description>
    <link>https://dev.to/artemy_and</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4148730%2Fffaab19c-9321-4a49-8e6b-fb9e803e7f03.jpg</url>
      <title>DEV Community: Artemy Andruschak</title>
      <link>https://dev.to/artemy_and</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artemy_and"/>
    <language>en</language>
    <item>
      <title>Ollama has no authentication: 5 ways to share it with your team safely</title>
      <dc:creator>Artemy Andruschak</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/artemy_and/ollama-has-no-authentication-5-ways-to-share-it-with-your-team-safely-2ong</link>
      <guid>https://dev.to/artemy_and/ollama-has-no-authentication-5-ways-to-share-it-with-your-team-safely-2ong</guid>
      <description>&lt;p&gt;Ollama has no API keys. It listens on &lt;code&gt;127.0.0.1:11434&lt;/code&gt; and answers whoever connects. On a laptop that's fine. The trouble starts when someone sets &lt;code&gt;OLLAMA_HOST=0.0.0.0&lt;/code&gt; so a colleague can use the GPU box under the desk, and from then on every endpoint is open to anyone who can reach it. Not only chat, but also pull, delete, create and push.&lt;/p&gt;

&lt;p&gt;A few numbers from this year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In January, SentinelOne and Censys &lt;a href="https://thehackernews.com/2026/01/researchers-find-175000-publicly.html" rel="noopener noreferrer"&gt;counted about 175,000 Ollama servers&lt;/a&gt; open to the internet in 130 countries. 48% of them had models with tool calling.&lt;/li&gt;
&lt;li&gt;In May, Cyera published &lt;a href="https://labs.cloudsecurityalliance.org/research-rb/csa-research-note-ollama-bleeding-llama-cve-2026-7482-202605/" rel="noopener noreferrer"&gt;Bleeding Llama (CVE-2026-7482)&lt;/a&gt;. Two unauthenticated requests, &lt;code&gt;/api/create&lt;/code&gt; with a crafted GGUF file and then &lt;code&gt;/api/push&lt;/code&gt; to the attacker's registry, leak the server's memory: environment variables, API keys, other people's prompts. The fix shipped in 0.17.1 back in February, but the release notes didn't call it a security fix, so plenty of servers never got updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't wait for keys in Ollama itself. In 2025 a maintainer &lt;a href="https://github.com/ollama/ollama/pull/9131" rel="noopener noreferrer"&gt;wrote under a PR that added them&lt;/a&gt;: "In the short-term we don't plan to add an API key system to the Ollama server itself. Usually I recommend using a VPN like tailscale." As of 0.40.3 (October 2026) there is still no server-side setting for it. There is an &lt;code&gt;OLLAMA_AUTH&lt;/code&gt; variable, but it only makes the &lt;code&gt;ollama&lt;/code&gt; client sign the requests it sends.&lt;/p&gt;

&lt;p&gt;So the protection has to sit in front of Ollama. Here are five ways to do that, from the simplest to the most complete, and what each one leaves open.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, check that yours isn't open
&lt;/h2&gt;

&lt;p&gt;From a machine outside your network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 http://YOUR_PUBLIC_IP:11434/api/tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prints a list of models, anyone can use them. Also run &lt;code&gt;ollama -v&lt;/code&gt;: anything older than 0.17.1 needs an update whatever else you decide.&lt;/p&gt;

&lt;p&gt;If Ollama runs in Docker, look at how the port is published. &lt;code&gt;-p 11434:11434&lt;/code&gt; listens on every interface, and Docker writes its own iptables rules, so a ufw rule that denies 11434 doesn't block it. Use &lt;code&gt;-p 127.0.0.1:11434:11434&lt;/code&gt;, or don't publish the port at all when only other containers need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep it on localhost and use SSH or Tailscale
&lt;/h2&gt;

&lt;p&gt;The least work: Ollama stays private and people come in over a channel that already has authentication.&lt;/p&gt;

&lt;p&gt;For one or two people, an SSH tunnel is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; 11434:localhost:11434 you@gpu-box
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clients then use &lt;code&gt;http://localhost:11434&lt;/code&gt; as if Ollama ran on their own machine.&lt;/p&gt;

&lt;p&gt;For a team, Tailscale is the usual answer, and it's what the Ollama maintainer suggested. Let Ollama listen on all interfaces (&lt;code&gt;sudo systemctl edit ollama&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"OLLAMA_HOST=0.0.0.0:11434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and let the firewall accept the port only from the tailnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="k"&gt;in &lt;/span&gt;on tailscale0 to any port 11434 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With ufw on and its default of denying incoming traffic, nothing else gets through. Tailscale ACLs then decide which people or devices can reach the box at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it leaves open:&lt;/strong&gt; anyone who gets in can use the whole API, including deleting models. Every request looks the same to Ollama, so you can't tell who is using the GPU or how much, and you can't take access away from one script without taking it from the person who runs it. Every user also has to install and sign in to Tailscale, and tools running in the cloud, like a hosted n8n, can't easily join.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A reverse proxy that checks a token
&lt;/h2&gt;

&lt;p&gt;If clients need a normal HTTPS address, put a proxy in front that refuses requests without a token. With Caddy, which gets the certificate by itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ollama.example.com {
    @no_token not header Authorization "Bearer {$OLLAMA_TOKEN}"
    respond @no_token 401

    # Nobody outside needs to create, push or delete models
    @model_admin {
        path /api/create /api/push /api/pull
        path /api/delete /api/copy /api/blobs/*
    }
    respond @model_admin 403

    reverse_proxy localhost:11434 {
        header_up Host {upstream_hostport}
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Caddy with a long random &lt;code&gt;OLLAMA_TOKEN&lt;/code&gt; in its environment. Ollama itself stays on &lt;code&gt;127.0.0.1&lt;/code&gt;. The &lt;code&gt;Host&lt;/code&gt; line matters: Ollama listening on localhost refuses requests addressed to any other host name, which is why the &lt;a href="https://github.com/ollama/ollama/blob/main/docs/faq.mdx" rel="noopener noreferrer"&gt;Ollama FAQ&lt;/a&gt; sets &lt;code&gt;Host localhost:11434&lt;/code&gt; in its nginx example. Without that line, Ollama answers 403 to any request that comes in under a real domain name. Streaming works as is.&lt;/p&gt;

&lt;p&gt;OpenAI-compatible clients (Continue, Cursor, n8n, the OpenAI SDKs) take the token as their API key with &lt;code&gt;https://ollama.example.com/v1&lt;/code&gt; as the base URL.&lt;/p&gt;

&lt;p&gt;Closing the model management endpoints is worth doing even with a token: they are the ones Bleeding Llama needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it leaves open:&lt;/strong&gt; one token for everyone. When someone leaves, you change it on every machine. You still can't see who sent what. You can give each person their own token with a &lt;code&gt;map&lt;/code&gt; in nginx or several matchers in Caddy, but then you are writing your own small gateway in a config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Open WebUI as the front door
&lt;/h2&gt;

&lt;p&gt;If your team already chats through Open WebUI, it can be the only way in. Ollama stays private, everyone gets an Open WebUI account (or signs in through your SSO), and people who need the API create personal keys.&lt;/p&gt;

&lt;p&gt;An admin turns keys on under &lt;strong&gt;Admin Settings → Authentication → API Keys&lt;/strong&gt;, non-admins also need the API Keys permission, and then each person creates a key in &lt;strong&gt;Settings → Account&lt;/strong&gt;. &lt;a href="https://docs.openwebui.com/features/authentication-access/api-keys" rel="noopener noreferrer"&gt;According to the docs&lt;/a&gt;, a key works with the same endpoints as the web interface: OpenAI-style &lt;code&gt;/api/chat/completions&lt;/code&gt;, and &lt;code&gt;/ollama/api/*&lt;/code&gt; passed through to Ollama.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it leaves open:&lt;/strong&gt; a key gets all the rights of the person who created it. Endpoint restrictions exist, but they apply to every key on the instance, not to one. The docs don't mention spending or rate limits per key, so an agent stuck in a loop can hold the GPU all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A proxy built for this: LoLLMs Hub or LiteLLM
&lt;/h2&gt;

&lt;p&gt;Two existing projects give each person a key without a hand-written config.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ParisNeo/lollms_hub" rel="noopener noreferrer"&gt;LoLLMs Hub&lt;/a&gt; by ParisNeo (it started as &lt;code&gt;ollama_proxy_server&lt;/code&gt;, and the Ollama README lists it as "Ollama Fortress") puts keys, rate limits and per-user usage stats in front of one or many Ollama servers, spreads load across several GPU machines, and lets you pull and delete models from its admin page. Apache-2.0. Pick it if you have more than one box.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/BerriAI/litellm" rel="noopener noreferrer"&gt;LiteLLM proxy&lt;/a&gt; gives each person a virtual key with a budget and a rate limit, and it talks to Ollama and to about a hundred cloud providers. Virtual keys need a Postgres database next to it, and it has a lot of settings. Pick it if someone on the team will look after it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they leave open:&lt;/strong&gt; one more service to run and update. That's true of options 2 and 3 as well, but these are bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Spillway (I built this one)
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I wrote &lt;a href="https://github.com/Artemy-And/spillway" rel="noopener noreferrer"&gt;Spillway&lt;/a&gt;, so take this section with that in mind.&lt;/p&gt;

&lt;p&gt;I built it for a 10–50 person company that has Ollama on one GPU box, also pays for Claude or OpenAI, and has nobody whose job is to run infrastructure. It's free and open source, single sign-on included, and nothing in it is held back for a paid plan. One container with SQLite and an admin page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;spillway &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;spillway
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/Artemy-And/spillway/main/docker-compose.yml
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OLLAMA_URL=http://ollama:11434"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .env
docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; ollama up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose logs spillway    &lt;span class="c"&gt;# prints the setup link&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Ollama container in that compose file publishes no port, so only Spillway can reach it. Spillway answers in Ollama's own format (&lt;code&gt;/api/chat&lt;/code&gt;, &lt;code&gt;/api/generate&lt;/code&gt;, &lt;code&gt;/api/embed&lt;/code&gt;, &lt;code&gt;/api/tags&lt;/code&gt;), as well as OpenAI's and Anthropic's, so Open WebUI, Claude Code, Codex and n8n connect with a key, without changes on their side. It doesn't forward &lt;code&gt;/api/create&lt;/code&gt;, &lt;code&gt;/api/push&lt;/code&gt; or &lt;code&gt;/api/delete&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;Every person, laptop or n8n workflow gets its own key with a daily and a monthly limit in dollars, which matter once cloud models sit behind the same key. For the GPU itself there's a rule that caps agent keys at so many requests per minute. The request log shows who sent what, with card numbers and similar personal data masked, and another rule blocks prompts with such data from cloud models while local ones still answer. When a team runs out of its cloud budget or a provider goes down, requests go to the local model instead of failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it leaves open:&lt;/strong&gt; it's young (version 0.4) and I'm the only maintainer. It's AGPL-3.0, which some companies don't allow. It doesn't spread load across several GPU machines. And if two people just share one GPU, it's more than you need: use option 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to pick
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Access per person&lt;/th&gt;
&lt;th&gt;Limits per key&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH / Tailscale&lt;/td&gt;
&lt;td&gt;per device, in Tailscale&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy with a token&lt;/td&gt;
&lt;td&gt;one shared token&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;an hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open WebUI keys&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;minutes, if you already run it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoLLMs Hub&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;rate&lt;/td&gt;
&lt;td&gt;an evening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteLLM&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;budget and rate&lt;/td&gt;
&lt;td&gt;an evening, plus Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spillway&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;dollars, and rate for agents&lt;/td&gt;
&lt;td&gt;minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A rule of thumb: two or three people who trust each other, Tailscale. People and scripts that need an HTTPS address, a proxy with the model management endpoints closed. More people than you can track in your head, or a mix of local and cloud models, something with a key per person.&lt;/p&gt;

&lt;p&gt;Whatever you choose, run Ollama 0.17.1 or later and make sure port 11434 doesn't answer from the internet. If you've got Ollama shared in another way that works well, I'd like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>selfhosted</category>
      <category>security</category>
      <category>ollama</category>
    </item>
    <item>
      <title>A self-hosted accessibility monitor that turns 66 errors into 13 things to fix</title>
      <dc:creator>Artemy Andruschak</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:14:01 +0000</pubDate>
      <link>https://dev.to/artemy_and/a-self-hosted-accessibility-monitor-that-turns-66-errors-into-13-things-to-fix-5fe2</link>
      <guid>https://dev.to/artemy_and/a-self-hosted-accessibility-monitor-that-turns-66-errors-into-13-things-to-fix-5fe2</guid>
      <description>&lt;p&gt;Since the European Accessibility Act started applying in June 2025, a lot of shops, agencies and small product teams have the same question: where do our sites actually stand, and is it getting better or worse?&lt;/p&gt;

&lt;p&gt;The tools for that tend to fall into two groups. Enterprise monitors do the whole job, at enterprise prices, with your data on their servers. Open-source scanners are free, but they either run once, or keep a history only for the URLs you add one by one.&lt;/p&gt;

&lt;p&gt;I wanted checking a site to be quick and easy, with a report that's clear and honest about what it covers. And I wanted it free, on my own server. So I built &lt;a href="https://github.com/Artemy-And/tabwalk" rel="noopener noreferrer"&gt;Tabwalk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Tabwalk scans whole sites daily or weekly with Playwright and axe-core, turns one template bug into one row instead of hundreds, and runs on your own server with one &lt;code&gt;docker compose up -d&lt;/code&gt;. Free and AGPL-3.0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0r56r0uw6wuh599kzqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0r56r0uw6wuh599kzqc.png" alt="A Tabwalk report for a demo shop" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finds pages.&lt;/strong&gt; Tabwalk reads &lt;code&gt;sitemap.xml&lt;/code&gt; (or &lt;code&gt;sitemap_index.xml&lt;/code&gt;). With no sitemap, it takes the links from the home page. Tracking parameters like &lt;code&gt;utm_*&lt;/code&gt; are stripped so the same page isn't checked twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks each page.&lt;/strong&gt; A worker opens it in headless Chromium through Playwright and runs axe-core with the WCAG 2.2 A/AA rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it again.&lt;/strong&gt; Every site is rescanned daily or weekly, and each scan is compared with the previous one: what's new, what got fixed, and how the count trends.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  One mistake counts once
&lt;/h2&gt;

&lt;p&gt;A template bug shows up on every page that uses the template. If a scanner reports it 500 times, nobody reads the report.&lt;/p&gt;

&lt;p&gt;So Tabwalk fingerprints every finding. It takes the element's markup, blanks out everything that changes from page to page, and hashes the result together with the rule id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ruleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;id|for|href|src|srcset|value|aria-labelledby|aria-describedby|aria-controls|data-&lt;/span&gt;&lt;span class="se"&gt;[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$1="*"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]{1,400}&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ruleId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two product cards with an empty link, &lt;code&gt;&amp;lt;a href="/p/1042" class="card-link"&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;a href="/p/2077" class="card-link"&amp;gt;&lt;/code&gt;, both become &lt;code&gt;&amp;lt;a href="*" class="card-link"&amp;gt;&lt;/code&gt;. Same rule, same fingerprint, one row with a page count.&lt;/p&gt;

&lt;p&gt;In the demo shop that ships with the repo, that turns 66 failing elements into 13 rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  It says what it can't judge
&lt;/h2&gt;

&lt;p&gt;axe-core returns a third category next to passes and violations: &lt;code&gt;incomplete&lt;/code&gt;. These are checks it can't decide on its own, like contrast over a background image.&lt;/p&gt;

&lt;p&gt;Tabwalk keeps them and labels them "needs a human". Automated checks cover only part of WCAG, so the honest thing for a tool to do is say which parts it measured and which parts are yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  No Redis, no cron container
&lt;/h2&gt;

&lt;p&gt;Scans are background jobs, and the usual answer is Redis + BullMQ, plus cron for the schedule. Tabwalk uses &lt;a href="https://github.com/timgit/pg-boss" rel="noopener noreferrer"&gt;pg-boss&lt;/a&gt; for both: the queue and the schedule live in the same Postgres as the data. Every 15 minutes the worker queues a scan for each site that is due.&lt;/p&gt;

&lt;p&gt;The whole thing is four containers: Postgres, API (Hono), worker and web (nginx serving the React dashboard):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Artemy-And/tabwalk.git
&lt;span class="nb"&gt;cd &lt;/span&gt;tabwalk
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it runs on your network, it can check staging and internal sites a cloud service can't reach. There's no account and no telemetry; results stay in your Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  In CI
&lt;/h2&gt;

&lt;p&gt;The same checker runs as a GitHub Action, without the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Artemy-And/tabwalk@v0.2.1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://staging.example.com&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serious&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job summary lists every problem. "Needs a human" results are listed too, but they never fail the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without a real site
&lt;/h2&gt;

&lt;p&gt;The repo includes a small demo shop with problems built in: images without alt text, icon buttons without names, a heading jump, unlabeled form fields, a video without captions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.demo.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;http://acmestore.example/&lt;/code&gt; as a site and run a scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't make a site compliant, and it doesn't change anything on it: no overlay, no widget. There's no login yet, pages behind a login aren't scanned, and reports can't be exported yet.&lt;/p&gt;

&lt;p&gt;Next up: plain-language explanations, turning the "needs a human" results into a checklist, and suggested code fixes. Suggested, never applied automatically.&lt;/p&gt;

&lt;p&gt;Tabwalk is free and AGPL-3.0. If you try it, I'd love to hear what's missing. Issues, discussions and pull requests are open, and there are a few &lt;a href="https://github.com/Artemy-And/tabwalk/labels/good%20first%20issue" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt; if you'd like to help. And if it's useful to you, a star on &lt;a href="https://github.com/Artemy-And/tabwalk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>opensource</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
