<?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: Mirko Stahnke</title>
    <description>The latest articles on DEV Community by Mirko Stahnke (@findnix).</description>
    <link>https://dev.to/findnix</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%2F4085708%2F61747ce6-843d-41a9-883f-b60fb25e1256.png</url>
      <title>DEV Community: Mirko Stahnke</title>
      <link>https://dev.to/findnix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/findnix"/>
    <language>en</language>
    <item>
      <title>Bring Your Own Key: Building an AI Portal That Never Touches the Billing</title>
      <dc:creator>Mirko Stahnke</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:47:18 +0000</pubDate>
      <link>https://dev.to/findnix/bring-your-own-key-building-an-ai-portal-that-never-touches-the-billing-2noh</link>
      <guid>https://dev.to/findnix/bring-your-own-key-building-an-ai-portal-that-never-touches-the-billing-2noh</guid>
      <description>&lt;h2&gt;
  
  
  Bring-your-own-key, not another subscription
&lt;/h2&gt;

&lt;p&gt;Every few months another "all AI models in one place" product shows up, and almost all of them work the same way: you pay them a subscription, and they resell access to Claude, GPT-4, Gemini and friends at a markup. That's a reasonable business, but it's not the only way to build this, and it's not the way &lt;a href="https://ai.findnix.eu" rel="noopener noreferrer"&gt;ai.findnix.eu&lt;/a&gt; works.&lt;/p&gt;

&lt;p&gt;AI Hub is a bring-your-own-key portal: you paste in your own Anthropic, OpenAI, Google, Mistral, Stability AI, ElevenLabs and Runway keys, and the portal just gives you one consistent interface — chat, image generation, text-to-speech, a code assistant, video generation — over whichever of those you've connected. Billing happens directly between you and the provider. The portal itself charges nothing for API usage, because it never touches the money side at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keys are the one thing that has to be paranoid
&lt;/h2&gt;

&lt;p&gt;The whole trust model of a BYOK product rests on one thing: can users believe their API keys — which are effectively bearer tokens for their own paid accounts — are safe. Every key is encrypted with AES-256-GCM before it touches the database, with a unique nonce per key (so two identical keys never produce identical ciphertext, which matters for preventing pattern analysis against the stored blobs). The master key lives in a config file that's explicitly blocked from direct HTTP access at the nginx level, as a second layer independent of PHP actually executing the file correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;encryptApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$plaintext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$key&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hex2bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MASTER_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$tag&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;openssl_encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'aes-256-gcm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;OPENSSL_RAW_DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'enc'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cipher&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'nonce'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$nonce&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;Nothing exotic — GCM gives you authenticated encryption for free, so a tampered ciphertext fails to decrypt rather than silently returning garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two identities, one login, no forced choice
&lt;/h2&gt;

&lt;p&gt;AI Hub is a sibling project to &lt;a href="https://findnix.eu" rel="noopener noreferrer"&gt;findnix.eu&lt;/a&gt;, a GDPR-focused EU search engine, and that created an actual product question: should AI Hub have its own user accounts, or piggyback on findnix.eu's existing ones?&lt;/p&gt;

&lt;p&gt;The answer ended up being both, deliberately. At signup you choose: a standalone AI Hub account, or one linked to your findnix.eu account with a shared login. Existing findnix.eu users don't have to fill out a second registration form at all — a link in their findnix.eu account silently provisions a linked AI Hub identity and drops them straight into the dashboard, authenticated.&lt;/p&gt;

&lt;p&gt;That "silently provisions" step runs over a short-lived, single-use signed token rather than any shared session or cookie trickery between the two domains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;consumeSsoToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* look up token, reject if used or older than 60s */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;mark&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$aiUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* look up existing linked account for this findnix user */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$aiUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$aiUserId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// first time: silently create a linked AI Hub account, no form involved&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createLinkedAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fnx_user_id'&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;For linked accounts, password checks go straight to findnix.eu's own password hash rather than a duplicated one — one password, one source of truth, even though the two products keep separate account tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Least privilege, even between your own apps
&lt;/h2&gt;

&lt;p&gt;Because AI Hub and findnix.eu now share a physical database (AI Hub's tables live in the same schema with their own prefix, rather than a separate database), it would have been easy to just reuse findnix.eu's own full-access database credentials for AI Hub too. Instead AI Hub gets its own MySQL user, scoped to &lt;code&gt;SELECT, INSERT&lt;/code&gt; on the shared users table (enough to look up and create linked accounts) and full access only to its own tables. It's internal plumbing nobody using the product will ever see, but it means a bug in a comparatively young, less-battle-tested part of the stack can't reach past its own tables into the main product's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The provider list — Claude, GPT-4o, Gemini, Mistral, DALL-E, Stability AI, ElevenLabs, Runway — covers the obvious ground, but "bring your own key" as a model scales naturally to whatever shows up next; adding a provider is a new small API wrapper file and an entry in a provider list, not a pricing renegotiation.&lt;/p&gt;

&lt;p&gt;If the BYOK model is something you'd rather use than another flat-rate AI subscription: &lt;a href="https://ai.findnix.eu" rel="noopener noreferrer"&gt;ai.findnix.eu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Community Wiki Around a Privacy-First Search Engine</title>
      <dc:creator>Mirko Stahnke</dc:creator>
      <pubDate>Sat, 22 Aug 2026 23:05:42 +0000</pubDate>
      <link>https://dev.to/findnix/building-a-community-wiki-around-a-privacy-first-search-engine-1pnd</link>
      <guid>https://dev.to/findnix/building-a-community-wiki-around-a-privacy-first-search-engine-1pnd</guid>
      <description>&lt;h2&gt;
  
  
  The problem: a search engine needs more than a search box
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://findnix.eu" rel="noopener noreferrer"&gt;findnix.eu&lt;/a&gt; is a EU-based, privacy-first search engine — "Find alles, speichere nix" (find everything, store nothing) is the tagline. Once real users started using it, the support questions started too: how does the spam filter work, why isn't my site indexed yet, what data do you actually store, how do I report a fake shop I found in the results?&lt;/p&gt;

&lt;p&gt;Answering the same questions over email doesn't scale, and a single flat FAQ page doesn't either once you also want space for longer-form, kid-safe explainer articles and general knowledge entries contributed by the community. So instead of bolting a FAQ page onto the main site, we split it out into its own project: &lt;strong&gt;wiki.findnix.eu&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three categories, one wiki
&lt;/h2&gt;

&lt;p&gt;The wiki is organized into three top-level groups, each with its own short-link in the header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FAQ findnix&lt;/strong&gt; — how the search engine, the spam filter, the DNSBL cross-check, and the points/rewards system work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kids findnix&lt;/strong&gt; — safe, curated explainer content that doubles as background material for &lt;a href="https://kids.findnix.eu" rel="noopener noreferrer"&gt;kids.findnix.eu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiki findnix&lt;/strong&gt; — general knowledge entries submitted by the community, reviewed before they go live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every entry goes through a &lt;code&gt;submit.php&lt;/code&gt; form and lands in a moderation queue with &lt;code&gt;status='approved'&lt;/code&gt; as the gate — nothing is public until someone actually looks at it. That single status column is doing a lot of quiet work: it's the difference between "anyone can publish anything" and "anyone can propose something."&lt;/p&gt;

&lt;h2&gt;
  
  
  Translation without an embedded widget
&lt;/h2&gt;

&lt;p&gt;The wiki needs to serve an EU-wide audience, and hard-coding 20+ language files for community-submitted content isn't realistic for a small team. The pragmatic fix: a language dropdown that builds a one-time link to Google's &lt;code&gt;translate.goog&lt;/code&gt; proxy for the current page, entirely client-side, on click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;wikiGoTranslate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&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;\.&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.translate.goog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&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;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&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="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_x_tr_sl=de&amp;amp;_x_tr_tl=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;_x_tr_hl=de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&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;No translation script is embedded or running in the background on the page itself — it's a single click handler that computes a URL and navigates. That turned out to matter more than expected: an earlier version embedded Google's live translation widget directly on the page, and it collided with the sticky header (the widget would inject its own top bar and push the layout around). Moving to a proxy-link model instead of an embedded widget removed an entire class of layout bugs, and it means one less third-party script running continuously against visitors' browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old links still have to work
&lt;/h2&gt;

&lt;p&gt;Categories used to be addressed by numeric ID (&lt;code&gt;?cat=12&lt;/code&gt;). Once the wiki grew nice slug-based URLs (&lt;code&gt;?cat_slug=faq-findnix&lt;/code&gt;), old bookmarks and inbound links with the numeric form still needed to resolve — so the numeric path does a lookup and issues a real 301 redirect to the slug URL rather than silently rendering the same content on two different URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$catId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$catSlug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$lookup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT slug FROM categories WHERE id=?'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$lookup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;bindValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$catId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SQLITE3_INTEGER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$lookup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SQLITE3_ASSOC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /k/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small detail, but it's the difference between a wiki that quietly accumulates duplicate/dead URLs over years and one that doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The wiki is deliberately simple: SQLite-backed, PHP, no JS framework, no build step. That's a feature, not a limitation — it means the barrier to contributing an entry, or to reading the code and understanding exactly what happens to a submission, stays low. As findnix.eu grows, the plan is to keep leaning on the community to write the FAQ and knowledge entries rather than trying to centrally author everything — the people hitting an edge case are usually the best people to explain it to the next person who hits it.&lt;/p&gt;

&lt;p&gt;If you want to see it in action (or contribute an entry), it's live at &lt;a href="https://wiki.findnix.eu" rel="noopener noreferrer"&gt;wiki.findnix.eu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Search Engine for Kids That Doesn't Track Them</title>
      <dc:creator>Mirko Stahnke</dc:creator>
      <pubDate>Sat, 22 Aug 2026 23:05:15 +0000</pubDate>
      <link>https://dev.to/findnix/building-a-search-engine-for-kids-that-doesnt-track-them-1j0g</link>
      <guid>https://dev.to/findnix/building-a-search-engine-for-kids-that-doesnt-track-them-1j0g</guid>
      <description>&lt;h2&gt;
  
  
  Search engines for kids usually mean one of two things
&lt;/h2&gt;

&lt;p&gt;Either a heavily filtered wrapper around a big commercial index (which still leaks through things a filter didn't anticipate), or a walled garden with a handful of hand-picked links that goes stale in a month. &lt;a href="https://kids.findnix.eu" rel="noopener noreferrer"&gt;kids.findnix.eu&lt;/a&gt; is an attempt at a third option: build and maintain &lt;strong&gt;our own curated index&lt;/strong&gt;, specifically for kids and teenagers, as a companion project to the main &lt;a href="https://findnix.eu" rel="noopener noreferrer"&gt;findnix.eu&lt;/a&gt; search engine.&lt;/p&gt;

&lt;p&gt;The homepage states the promise plainly: &lt;em&gt;"no data is stored, and we only try to present vetted, educationally valuable content."&lt;/em&gt; Two mascots, Uli and Nele, sit on either side of the search box — which sounds like a small detail, but user testing with actual kids made clear that a friendly, recognizable interface matters as much as the filtering underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Curation is a crawling problem, not a filtering problem
&lt;/h2&gt;

&lt;p&gt;Rather than filtering an existing general-purpose index after the fact, kids.findnix.eu runs its own dedicated crawlers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kids_page_image_crawler.php&lt;/code&gt; / &lt;code&gt;kids_page_video_crawler.php&lt;/code&gt; — pull images and videos specifically from sources already cleared for the kids index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crawl_sitemap_articles.php&lt;/code&gt;, &lt;code&gt;crawl_topic_pages.php&lt;/code&gt;, &lt;code&gt;crawl_wissenskarten.php&lt;/code&gt; — walk sitemaps and topic pages of vetted educational sites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crawl_wp_generic.php&lt;/code&gt; — a generic crawler for WordPress-based educational sites, since a huge share of school/educational content in German-speaking countries runs on WordPress&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;classify_sonstiges_llm.php&lt;/code&gt; — an LLM-assisted classification pass for the long tail of content that doesn't fit a hand-written rule&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result, shown live on the homepage, is a running count rather than a marketing number: pages indexed, images, videos, links. It updates because the crawlers keep running, not because someone typed a number into a template once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search box is only half the product
&lt;/h2&gt;

&lt;p&gt;What's easy to miss from the name is that kids.findnix.eu isn't just a search box with a filter — half the site is interactive, kid-facing tools built around learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flag Quiz&lt;/strong&gt; (&lt;code&gt;flaggenquiz.php&lt;/code&gt;) — geography, gamified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret Code Workshop&lt;/strong&gt; (&lt;code&gt;geheimcode_werkstatt.php&lt;/code&gt;) — classic ciphers, taught by building them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Riddle Challenge&lt;/strong&gt; (&lt;code&gt;knobel_challenge.php&lt;/code&gt;) — logic puzzles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train your own AI&lt;/strong&gt; (&lt;code&gt;ki_trainer.php&lt;/code&gt;) — a hands-on, simplified intro to how machine learning classification actually works, aimed at demystifying "AI" rather than treating it as magic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Adventure&lt;/strong&gt; (&lt;code&gt;code_abenteuer.php&lt;/code&gt;) — a first, gentle introduction to programming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these need the search index at all — they're standalone reasons to visit the site directly rather than only arriving via a search query, which matters for a product whose whole pitch is &lt;em&gt;"safe corner of the internet for kids,"&lt;/em&gt; not just &lt;em&gt;"safe search results."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility gets its own crawler, too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;a11y_crawler.php&lt;/code&gt; checks the site's own HTML structure for accessibility issues — worth calling out specifically because it's easy for a "kids" product to over-index on colorful UI and under-index on actually being usable by kids with different needs. Running the accessibility check as its own recurring job, rather than a one-time manual pass, keeps it from silently rotting as pages get added.&lt;/p&gt;

&lt;h2&gt;
  
  
  No data stored means no data stored
&lt;/h2&gt;

&lt;p&gt;The "no data stored" line on the homepage isn't just copy — visit counting, for example, is deliberately coarse: one cookie-gated counter per visit session (30 minutes), not a log line per page view, and no data ends up tied to an individual beyond a hashed IP used only to avoid double-counting the same visit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_COOKIE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'kids_visited'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;kids_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"INSERT INTO fnx_kids_stats (type, referrer, url, ip_hash, lang, user_agent)
         VALUES ('visit', ?, ?, ?, ?, ?)"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nb"&gt;setcookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kids_visited'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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;That's the whole point of building a dedicated kids product instead of just adding a "kids mode" toggle to the main search engine: every default — logging, cookies, what gets shown, what gets stored — gets re-examined for a young audience specifically, instead of inherited from an adult product and only trimmed down after the fact.&lt;/p&gt;

&lt;p&gt;kids.findnix.eu is live at &lt;a href="https://kids.findnix.eu" rel="noopener noreferrer"&gt;kids.findnix.eu&lt;/a&gt;, EU-hosted, and — like the rest of the findnix.eu family — built to be small enough that you can actually read the whole thing and know what it does.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>findnix.eu: A GDPR-Compliant Search Engine Built From Scratch</title>
      <dc:creator>Mirko Stahnke</dc:creator>
      <pubDate>Wed, 19 Aug 2026 23:23:57 +0000</pubDate>
      <link>https://dev.to/findnix/findnixeu-a-gdpr-compliant-search-engine-built-from-scratch-39d2</link>
      <guid>https://dev.to/findnix/findnixeu-a-gdpr-compliant-search-engine-built-from-scratch-39d2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every major search engine today runs on the same trade: you get results, they get your data. findnix.eu is an attempt to break that trade — a search engine built in the EU, for the EU, that doesn't track you, doesn't build a profile of you, and doesn't sell what it doesn't collect in the first place.&lt;/p&gt;

&lt;p&gt;This isn't a pitch deck. It's a working search engine, built and run by one person, and this post is a quick tour of what it actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can search
&lt;/h2&gt;

&lt;p&gt;findnix.eu isn't just web search. Over time it's grown into a set of focused search verticals, each with its own index:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web&lt;/strong&gt; — general search, combining multiple sources with a preference for sites that submit their own sitemap (a small way to give independent sites a fairer shot than they'd get elsewhere)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;News&lt;/strong&gt; — pulled directly from public-broadcaster media libraries across the EU (ARD, ZDF, Arte, ORF, SRF and more), with a built-in video player — no detour through the broadcaster's own site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Podcasts&lt;/strong&gt; — full show and episode search via the Podcast Index API, with direct in-browser playback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images &amp;amp; Videos&lt;/strong&gt; — licensed, freely usable media&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Science&lt;/strong&gt; — open-access papers via OpenAIRE and BASE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firmen (Companies)&lt;/strong&gt; — a growing local business index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kultur&lt;/strong&gt; — European cultural heritage via Europeana&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a companion site, &lt;strong&gt;kids.findnix.eu&lt;/strong&gt;, built specifically for child-safe search — same philosophy, different audience.&lt;/p&gt;

&lt;h2&gt;
  
  
  No tracking, stated plainly
&lt;/h2&gt;

&lt;p&gt;No tracking cookies, no IP logging, no cross-session profiling. Search history lives in your browser's local storage only, never on the server. Ads (yes, there are ads — servers cost money) are flat-rate and clearly labeled, not auction-based, and capped at 3 per page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's newest
&lt;/h2&gt;

&lt;p&gt;The latest addition is a small marketplace feature: a &lt;strong&gt;domain valuation and sale tool&lt;/strong&gt;. You get a free (unbinding, heuristic) valuation of a domain based on TLD, length, and structure, and can list it for sale for a small amount of points — findnix's internal currency, earned through site activity. Inquiries land in an in-app inbox rather than exposing your email address publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build this
&lt;/h2&gt;

&lt;p&gt;Mostly because the alternative — accepting that "free" search always means being the product — never sat right. findnix.eu won't out-scale Google. It doesn't try to. It's trying to be a genuinely different deal: you search, we don't remember, and the site stays funded through ads you can actually see and understand, not ones built on a profile of you.&lt;/p&gt;

&lt;p&gt;If that sounds like your kind of search engine, give it a try: &lt;a href="https://findnix.eu" rel="noopener noreferrer"&gt;findnix.eu&lt;/a&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>php</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
