Automating BIND9 DNS Management: From Manual Configs to Infrastructure as Code

Managing BIND9 DNS zone files manually doesn’t scale. Every change means SSH-ing into a server, editing config files, and hoping you don’t break DNS resolution for your entire infrastructure.

I built an open-source stack that turns BIND9 management into proper Infrastructure as Code. Here’s what the stack looks like:

The Architecture

The solution has three components that work together:

  1. bind9-api — A REST API that sits on top of BIND9, providing HTTP endpoints for zone management, record CRUD, ACL management, DNSSEC, and more.

  2. Terraform Provider for BIND9 — A Terraform/OpenTofu provider that talks to the API, letting you manage DNS zones and records as code with full plan/apply workflow.

  3. Ansible Role — Handles the one-time deployment of both BIND9 and the API on your servers.

The key design decision: Ansible manages infrastructure (installing BIND9, deploying the API) while Terraform manages content (zones, records, ACLs). Each tool does what it’s best at.

What You Can Do

  • Manage 30+ DNS record types (A, AAAA, CNAME, MX, TXT, SRV, CAA, and more)
  • Single-server or multi-primary architectures
  • DNSSEC key management (KSK, ZSK, CSK)
  • ACL management as code
  • Bulk record generation using Terraform’s for_each and range()
  • Prometheus metrics for monitoring

Getting Started

The full walkthrough with architecture diagrams, code examples, and step-by-step setup is in the blog post on Dev Genius:

Read the full article on Medium

All three projects are open source under Apache 2.0. Contributions and feedback welcome!

Self-Hosting a Vaultwarden Password Manager

xkcd 936: Password Strength

Full text can also be viewed here.

Password vaults are a convenient and secure way to manage multiple passwords. As data breaches become more and more common, security guidance changes lead to an inevitable mishmash of credentials that are impossible to remember when not used daily. The logic behind constantly-evolving password guidelines is beyond the scope of this guide, but recent word from NIST on password vaults recommends their use:

Verifiers SHALL allow the use of password managers and autofill functionality. Verifiers SHOULD permit claimants to use the “paste” function when entering a password to facilitate password manager use when password autofill APIs are unavailable. Password managers have been shown to increase the likelihood that subscribers will choose stronger passwords, particularly if the password managers include password generators

This guide walks through setting up a lightweight, locally-hosted password manager that allows the user (or users) to keep their accesses secure without needing to perform superhuman feats of memorization. It is written assuming the reader has a fully configured WireGuard VPN server on their home network, with all local traffic handled by the VPN specifically. This is necessary for security, not just for Vaultwarden; it’s generally much simpler and safer to stand up your devops infrastructure when you don’t have to open ports to the entire internet.

Background

A password vault (or password manager) is a secure, encrypted digital storage system for username and password combinations. Remembering one password is much simpler than having to remember multiple. Why NIST took so long to come to this conclusion is out of scope for this guide, but feel free to enjoy others’ discourse on the topic.

xkcd 936: Password Strength
Source: xkcd 936

There are several password managers to choose from; Bitwarden is a strong contender in the space, and I highly recommend it for the average person or enterprise. It is open source, has weathered security audits, and you can even self-host it. While I personally dislike hosting anything on the cloud, I don’t have an issue with their model from a security perspective; your data is encrypted locally before it’s even sent to their servers, so even Bitwarden themselves could not access your information.

Some downsides exist, though; it’s a resource hog when hosted locally, and resources matter when you keep stacking services on your home Debian device. The average user does not need the scalability and infrastructure that it provides. Additionally, some services such as autofilling passwords require a paid subscription, even when self-hosting Bitwarden.

Thus, this guide settles on Vaultwarden. Vaultwarden is a lightweight re-write of Bitwarden in Rust. It uses much less CPU and RAM (~50MB to Bitwarden’s ~1-2GB), and as a bonus it is easier to set up. The process is explained below; the length of this write-up should speak to its simplicity.

Prerequisites

  • WireGuard VPN, configured to handle local address traffic.
  • Linux server (Debian, can be the same machine hosting the WireGuard VPN).
  • Basic command line familiarity.

Initial Setup

On the server meant to host the Vaultwarden instance, install podman and set up the necessary folder structure in your home directory. You will also need to set up a self-signing certificate (acceptable since we are on a VPN). Note that docker is also an option and most guides reference it; I prefer podman since it is more secure by not requiring root access and not having a single point of failure (dockerd).

First, find your server’s IP address. You will need to replace 192.168.1.100 below with your server’s IP address. With the correct IP used, run:

sudo apt update && sudo apt upgrade -y
sudo apt install podman podman-compose git -y
mkdir -p ~/vaultwarden/{data,config,ssl}
cd ~/vaultwarden
echo "ADMIN_TOKEN=$(openssl rand -hex 32)" > .env
chmod 600 .env
openssl req -x509 -newkey rsa:4096 -keyout ssl/private.key -out ssl/certificate.crt -days 36500 -nodes -subj "/CN=192.168.1.100"
chmod 600 ssl/private.key
chmod 644 ssl/certificate.crt

The certificate expires in 100 years (36,500 days). Expiration cannot be disabled completely, but if you have it set for a year your password manager may break and you could waste hours debugging the self-signed certificate you’ve since forgotten about.

You may also need to open the HTTPS port on your firewall if you use UFW. Run sudo ufw status and if it shows as active, run:

sudo ufw allow from 192.168.1.0/24 to any port 8443

Configuration

Still within the ~/vaultwarden folder, create docker-compose.yml by running the following (again, replace 192.168.1.100 with the correct IP address):

cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  vaultwarden:
    image: docker.io/vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    env_file:
      - .env
    environment:
      WEBSOCKET_ENABLED: 'true'
      SIGNUPS_ALLOWED: 'true'
      DOMAIN: 'https://192.168.1.100:8443'
      ROCKET_TLS: '{certs="/ssl/certificate.crt",key="/ssl/private.key"}'
      LOG_LEVEL: 'warn'
      EXTENDED_LOGGING: 'true'
    volumes:
      - ./data:/data
      - ./ssl:/ssl:ro
    ports:
      - "8443:80"
      - "3012:3012"
EOF

Now run the container and check status:

podman-compose up -d
podman ps

You should see the docker.io/vaultwarden/server:latest container running.

Account Setup

Access the admin console by typing https://192.168.1.100:8443 (replace the IP address) into a browser. You will be prompted to create the username and password, and then download the bitwarden password manager addon. It works perfectly with Vaultwarden and autofills your passwords, so this is highly recommended.

Next, configure it for your self-hosted server when logging in by selecting ‘self-hosted’ at login rather than ‘bitwarden.com’. Use the admin console URL/port again, and log in using the login created earlier.

Automated Start/Restart of Server

The last step here is to ensure Vaultwarden starts back up if the server reboots or has a power failure. This is also very simple to set up by running the following:

systemctl --user enable podman.service
sudo loginctl enable-linger $USER

podman generate systemd --new --name vaultwarden --files
mkdir -p ~/.config/systemd/user
mv container-vaultwarden.service ~/.config/systemd/user/

podman-compose down
systemctl --user daemon-reload
systemctl --user enable container-vaultwarden.service
systemctl --user start container-vaultwarden.service

systemctl --user status container-vaultwarden.service
podman ps

The above code sets up auto-start configuration and generates a systemd file for the vaultwarden service, then brings the Vaultwarden container down to switch over from podman-compose management to systemd management. The last two lines check status; if these look good, you are done.

Conclusion

You should now be able to use Vaultwarden in the browser of your choice. It should (when logged in) prompt you with the option to save passwords, and autopopulate them as needed. I hope this was helpful; if you notice any issues with this guide or process, please feel free to reach out or leave a comment.

My Experiences from NBX and Privacy Hell For Crypto….

nbx-conference

Hello Dev.to Community !

It’s been a while since my recent post on the platform. Today I would like to share with you my experience from NBX (Next Block Expo) event, that was organized in Warsaw, Poland. Although this post will not be filled so much with technical or programming knowledge, I just want to share my view on the conference, from my perspective and couple of thoughts that came into my mind.

Introduction

First, let’s define what Next Block Expo is ? NBX or Next Block Expo is an cryptocurrency-focused event, where all sorts of people from blockchain-technology gather in one place, where there are lectures for newbies or also more advanced market-participants, projects present their product and many more. And by having said all sorts of people, I mean literally ALL SORTS OF PEOPLE, from investors, businessmen/women, CEOs, Journalists, and even politicians through software-devs, people that have no clue what blockchain actually is, scammers, moon-boiz, marketing guys to companies that had their stands at the event.

What companies/projects where at the event ?

It actually needs to be pointed out, that this NBX-edition was combined together with an AI-Summit event, although there were only a couple of companies, having their stands that were focused on AI particularly, and not being an “AI-company” only because they added an tranding OpenClaw-Bot to aid customer’s portfolio.

A lot of the stands were either belonging to B2B-focused companies that help businesses to implement crypto-payments, offering immediate-conversion from crypto to fiat-transfers, off-ramp and on-ramp and this sort of stuff, or exchanges there were BingX, Bybit, ZondaCrypto, Kanga Exchange.

Besides that, what really has thrilled me, is that there were 4 companies or projects that I kept eye on, that were in some-sense delivering or willing to deliver privacy-extending product or they care about privacy.

I will list them out shortly:

  • Relocatify – A company that provides help while opening a bank account in Cambodia and purchase of an real-estate in Cambodia. Why Cambodia is somehow related to privacy ? It’s because they do not have KYC once you want to purchase/sell crypto.

Their financial system is outside of the CRS-system, so they do not report your financial activity. Does it sound like a safe-heaven for criminals ? Well technically yes, but remember criminals were/are/and unfortunately will exist everywhere but will use different methods for their operation.

For me living in EU, where surveillance grows and privacy already seems as a luxury and one needs to fight for it, usually with miserable outcome.

  • Sealed – A private messenger, that is using Falcon as key-signature algorithm, that provides quantum-resistant. It’s still developing and I had an opportunity to test-out the MVP.
    For now it’s going to be supporting only text-messages, but the founders say, that they work on implementing features that support images and documents. The messages will be safely stored on blockchain. The blockchain they are going to use is Algorand.

  • Gen6 – It’s a blockchain that already has it’s collective of Dapps like NCrypt, a private messenger (does not store messages on blockchain and is free to use). There is also RealSeal signs and timestamps your work on-chain. Gen6 Me Identity as an alternative to linktree, but on-chain. And many other products.

  • Neti – A company that is actually all into blockchain and privacy. They are those guys, who can manage everything from tokenization, ZK-Proofs, Privacy tools to Building a Blockchain from ground up. I asked if they have any intern-program and the lady I talked to, has said that it’s limited but they are about to organize hackathons and blockchain-dev community.

My View on freedom and privacy in Europe and Cryptocurrencies in Europe

Although I talked to a lot of cool people from the industry, collected a lot of merch as you can see on the pictures:

Although I talked to a lot of cool people from the industry, collected a lot of merch as you can see on the pictures:

nbx-conference

Then one thing is certain for me. When it comes to Europe, privacy of your financial-matters, does not already exist in Europe. And there is no sign of hope to change this trend, as surveillance will only grow and grow in this region, together with stupid regulations taking away your freedom.

The privacy for cryptocurrencies in Europe is gone, because every for transaction even in ATM or in Exchange-Points, will require from you to go through KYC, which I personally hate and find it only as a part of mass-surveillance.

It’s actually for me disastrous to see, how cryptocurrencies from technology that was about to threaten the entire monetary system and became only an assets or alternative form of payment.

expectations-reality

I think this image explains everything, better than anyone would.

My Personal Experiences

To the event its-self, I’d say I’m disappointed, this time organizers have decided to care more about quantity and not quality of the event, although indeed there were cool projects, this event seemed as a ghost-town for me and the venue was for me not as intuitive as previous one.

The organizers indeed announce a big success, on the event there were more than 2500 people. But the venue for me was a shot in the foot, because of the amount of free space and the actual design of the halls.

Merch-from-nbx

I realized also, there is not so much to learn for me from the panels or lectures, but that’s was obvious to me and tremendously outraged when one of the organizer in the interview said, that those speeches, panels are rather as an add-on and more it’s focused for businesses to be a gateway for new connections and so on.

Is it good ? Is it bad ? I guess as always, depends on the personal perspective. I also did not really like the fact that there has been a separate event for people who speak Ukrainian or Russian.

Don’t get me wrong, I do not have anything against any nation, I don’t care where are you from and what is your skin color, though organizing an separated event solely for certain language-speaking people, this is for a cause that splits people and separates from each other.

Conclusion: I do not like big places e.g. halls and non-technical talks. It was my last NBX-event attended and will look definitely look for technical events or those that are within my field of interest. So cryptography, privacy, blockchain and cybersecurity.

What about you ?

What have you guys been up to recently ? Have you been building some project or have you been doing something else ? Let me know down in the comments and feel free to share your opinion on any part of my article.

Cheers for reading, till next one 😀

I Built a Coordination System for Multiple Claude AI Agents — So They Stop Overwriting Each Other

I Built a Coordination System for Multiple Claude AI Agents — So They Stop Overwriting Each Other

Here’s a problem nobody talks about yet but everyone will hit as AI-assisted development scales up: what happens when two Claude agents are working in the same codebase at the same time?

They overwrite each other. Silently. No warnings, no errors — just lost work.

I built a skill to fix this. It’s called agent-comms, and it gives Claude agents a shared communication channel so they can coordinate before conflicts happen.

The Problem

I’ve been running multiple Claude Code sessions simultaneously on a project — one handling frontend UI work, another fixing the backend pipeline. Both intelligent, both fast, both completely unaware the other exists.

The result: one agent refactors a file, the other overwrites it ten seconds later with a different version. Neither agent knows. The work is gone.

The root issue: Claude agents have no shared awareness. Each terminal is its own isolated context. There’s no built-in coordination layer.

The Solution: File-Based Agent Communication

The only channel multiple Claude agents share is the filesystem. So that’s where I built the communication layer.

agent-comms creates a .agent-comms/ directory in your project root with three JSON files:

.agent-comms/
  registry.json       — who is active right now
  announcements.json  — what files each agent plans to edit (and why)
  messages.json       — direct agent-to-agent messages

A Python script (comms.py) handles all reads and writes — atomic, stale-safe, zero dependencies.

The Protocol

Every agent follows the same lifecycle:

1. Register

python3 $COMMS register researchUI "building dashboard UI components"
python3 $COMMS register Worker1 "fixing backend data pipeline"

2. Announce upfront — before touching any file

python3 $COMMS announce researchUI 
  "src/lib/api.ts|adding stream endpoint" 
  "src/pages/dashboard.tsx|adding real-time chart"

python3 $COMMS announce Worker1 
  "src/lib/api.ts|fixing broken API call" 
  "src/pages/dashboard.tsx|removing hardcoded mock data"

3. Check for conflicts

python3 $COMMS check-conflicts Worker1

Output:

CONFLICT: researchUI also needs src/lib/api.ts — their reason: adding stream endpoint
CONFLICT: researchUI also needs src/pages/dashboard.tsx — their reason: adding real-time chart component

Two conflicts detected. Before a single file was touched.

4. Negotiate

Worker1 (the later announcer) initiates:

python3 $COMMS send Worker1 researchUI 
  "CONFLICT on api.ts — my change: fixing broken endpoint call on line 34. I should go first since it's a bug fix."

python3 $COMMS send Worker1 researchUI 
  "CONFLICT on dashboard.tsx — removing 3 hardcoded mock arrays. Safe to do before your chart work. Suggest: I go first."

researchUI reads and replies:

python3 $COMMS read-messages researchUI
# FROM [Worker1]: CONFLICT on api.ts...
# FROM [Worker1]: CONFLICT on dashboard.tsx...

python3 $COMMS send researchUI Worker1 
  "Agreed — go first on both. I'll wait for your DONE broadcast."

5. Worker1 finishes and broadcasts

python3 $COMMS done Worker1 
  "src/backend/classifier.py" 
  "src/lib/api.ts" 
  "src/pages/dashboard.tsx"

6. researchUI gets the all-clear

python3 $COMMS read-messages researchUI
# FROM [Worker1]: [DONE] Worker1 finished. Modified: src/lib/api.ts, src/pages/dashboard.tsx

researchUI now knows exactly which files were modified and can proceed safely.

Full Terminal Output

Here’s the actual output from a real test run:

=== researchUI announces its files ===
[agent-comms] Announced 3 file(s) for 'researchUI'

=== Worker1 announces its files ===
[agent-comms] Announced 3 file(s) for 'Worker1'

=== researchUI checks for conflicts ===
CONFLICT: Worker1 also needs src/lib/api.ts — their reason: fixing broken API call
CONFLICT: Worker1 also needs src/pages/dashboard.tsx — their reason: removing hardcoded mock data

=== Worker1 negotiates ===
[agent-comms] Sent from 'Worker1' to 'researchUI'
[agent-comms] Sent from 'Worker1' to 'researchUI'

=== researchUI reads messages ===
  FROM [Worker1] @ 2026-03-27T19:14:27: CONFLICT on api.ts — fixing broken endpoint call on line 34. I should go first.
  FROM [Worker1] @ 2026-03-27T19:14:27: CONFLICT on dashboard.tsx — removing 3 hardcoded mocks. Suggest: I go first.

=== researchUI agrees ===
[agent-comms] Sent from 'researchUI' to 'Worker1'

=== Worker1 finishes ===
[agent-comms] 'Worker1' done. Broadcast sent to 1 agent(s).

=== researchUI gets the all-clear ===
  FROM [Worker1]: [DONE] Worker1 finished. Modified: src/backend/classifier.py, src/lib/api.ts, src/pages/dashboard.tsx

=== Final status ===
[agent-comms] Active agents: 1 | Unread messages: 0
  [researchUI] building dashboard UI components
    planned files: dashboard.tsx, Chart.tsx, api.ts

Zero overwrites. Zero lost work.

How It’s Designed as a Claude Code Skill

The system ships as a SKILL.md — a Claude Code skill file that instructs Claude to follow the agent lifecycle automatically. When you invoke superpowers:agent-comms in a Claude Code session, Claude:

  1. Runs status to see who’s active
  2. Auto-assigns itself a name (or asks you to rename it)
  3. Registers itself with its task description
  4. Before any edit session — announces files and checks conflicts
  5. If conflict detected — initiates or responds to negotiation
  6. Every ~10 tool calls — sends a heartbeat
  7. When done — deregisters and broadcasts to all other agents

The SKILL.md protocol:

## Step 2: Announce Upfront (BEFORE any file edit)

Before touching ANY file this session, list every file you plan to edit and why:

python3 $COMMS announce agent-N 
  "path/to/file.ts|reason for editing"

Then immediately check for conflicts:
python3 $COMMS check-conflicts agent-N

If output is CLEAR → proceed.
If output contains CONFLICT → go to Step 3 (negotiation).

Design Decisions

Why file-based?
Claude agents in different terminals share nothing except the filesystem. No sockets, no shared memory, no APIs between them. Files are the only viable channel.

Why upfront announcements instead of locks?
Locks only catch conflicts at the moment of editing — after you’ve already started work. Announcements catch them before any work begins, when the cost of changing course is zero.

Why negotiation instead of blocking?
Blocking wastes time. Negotiation lets agents make intelligent decisions: “include my change,” “I’ll go first,” or “let’s split the file.” The agents have context about what they need — use it.

Stale agent cleanup:
Agents that haven’t sent a heartbeat in 5 minutes are auto-removed from the registry. If an agent crashes without deregistering, it won’t block others indefinitely.

Project-scoped:
.agent-comms/ lives in the project root and is added to .gitignore automatically. Coordination state doesn’t belong in git history.

Install

SKILL_DIR="$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.5/skills/agent-comms"
mkdir -p "$SKILL_DIR/tests"
curl -o "$SKILL_DIR/comms.py" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-comms/main/comms.py
curl -o "$SKILL_DIR/SKILL.md" https://raw.githubusercontent.com/LakshmiSravyaVedantham/agent-comms/main/SKILL.md

Then in any Claude Code session working in a shared project:

superpowers:agent-comms

Tests

19 tests covering the full lifecycle:

python3 -m pytest tests/ -v
# 19 passed in 1.83s

What’s Next

  • Three-agent coordination — tested with 2, curious how negotiation chains work at 3+
  • File section locks — “I own lines 1–80 of this file, you take 81–160”
  • Integration with Claude Code hooks — auto-register/deregister via session hooks

GitHub

github.com/LakshmiSravyaVedantham/agent-comms

Python 3.8+, stdlib only, MIT license. Drop it into your superpowers skill directory and your agents start coordinating immediately.

If you’re running multiple Claude sessions simultaneously, this will save you from the silent overwrite problem sooner or later.

Understanding Attention Mechanisms – Part 2: Comparing Encoder and Decoder Outputs

In the previous article, we explored the main idea of attention and the modifications it requires in an encoder–decoder model. Now, we will explore that idea further.

An encoder–decoder model can be as simple as an embedding layer attached to a single LSTM. If we want a more advanced encoder, we can add additional LSTM cells.

Now, we initialize the long-term and short-term memory in the LSTMs of the encoder with zeros.

If our input sentence, which we want to translate into Spanish, is “Let’s go”, we can feed a 1 for “Let’s” into the embedding layer, unroll the network, and then feed a 1 for “go” into the embedding layer.

This process creates the context vector, which we use to initialize a separate set of LSTM cells in the decoder.

All of the input is compressed into the context vector.

But the idea of attention is that each step in the decoder should have direct access to the inputs.

So, let’s understand how attention connects the inputs to each step of the decoder.

In this example, the first thing attention does is determine how similar the outputs from the encoder LSTMs are to the outputs from the decoder LSTMs at each step.

In other words, we compute a similarity score between the LSTM outputs (the short-term memory or hidden states) from the encoder and the decoder.

For instance, we calculate a similarity score between:

  • The LSTM output from the first step in the encoder, and
  • The LSTM output from the first step in the decoder

We also calculate a similarity score between:

  • The LSTM output from the second step in the encoder, and
  • The LSTM output from the first step in the decoder

There are various ways to calculate this similarity.

One simple method is cosine similarity, which measures how similar two sequences of numbers (representing words) are.

We will explore this further in the next article.

Looking for an easier way to install tools, libraries, or entire repositories?
Try Installerpedia: a community-driven, structured installation platform that lets you install almost anything with minimal hassle and clear, reliable guidance.

Just run:

ipm install repo-name

… and you’re done! 🚀

Installerpedia Screenshot

🔗 Explore Installerpedia here

Which index should SQLite use?

git-lrc

Hello, I’m Maneshwar. I’m building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.

Even when indexes exist, choosing the wrong one can slow down a query significantly.

The optimizer’s job here is not just to use an index, but to use the right index.

One Table, One Index (Mostly)

For each table in a query, SQLite can typically use only one index.

There is one exception.

In OR-based queries, SQLite may use multiple indexes, but in most cases, it selects a single index per table.

Because of this limitation, index selection becomes a critical decision.

SQLite tries to ensure that at least one useful index is applied to each table whenever possible.

When Multiple Indexes Exist

Consider a table with multiple indexes:

“`sql id=”9j9h8h”
CREATE TABLE table1(x, y, z);
CREATE INDEX i1 ON table1(x);
CREATE INDEX i2 ON table1(y);




Now look at this query:



```sql id="d1km7x"
SELECT z FROM table1 WHERE x = 5 AND y = 6;

SQLite has two choices:

  • Use index i1 to find rows where x = 5, then filter y = 6
  • Use index i2 to find rows where y = 6, then filter x = 5

Both approaches are valid, but they may have very different costs.

How SQLite Chooses the Best Index

SQLite estimates how much work each option will require and chooses the one with the lowest cost.

The decision is based on heuristics such as:

  • How many rows are expected to match
  • How selective the index is
  • How much filtering is needed after lookup

If statistical data is available, SQLite makes better decisions.

This is where the sqlite_stat1 table comes into play.

It stores information about how many rows are typically associated with a given column value.

Using this data, SQLite can estimate which index will reduce the result set the most.

The index that is expected to return fewer rows is usually preferred.

Forcing SQLite to Ignore an Index

Sometimes, you may want to override SQLite’s choice.

SQLite provides a subtle mechanism for this using the unary + operator.

Example:

“`sql id=”u4cmnf”
SELECT z FROM table1 WHERE +x = 5 AND y = 6;




The `+` operator does nothing functionally, but it prevents SQLite from using the index on column `x`.

This forces the optimizer to consider other indexes, such as `i2` on column `y`.

This is a lightweight way to guide the optimizer without changing query semantics.

## Balancing WHERE and ORDER BY

Index selection is not only about filtering rows. 

SQLite also considers sorting requirements.

For example:



```sql id="z2t3r8"
SELECT * FROM table1 WHERE x = 5 ORDER BY y;

Now SQLite has a trade-off:

  • Use an index on x to filter efficiently
  • Use an index on y to avoid sorting

SQLite evaluates both options and chooses the one that results in the fastest overall execution.

This means sometimes SQLite may sacrifice a slightly less efficient filter to avoid an expensive sort operation.

When Sorting Cannot Use an Index

If no suitable index can satisfy the ORDER BY clause, SQLite must sort the results manually.

This is done using a temporary sorter, which behaves like a transient index.

The process looks like this:

open sorter
where-begin
    extract required columns
    build a record
    generate sort key
    insert into sorter
where-end
sort
for each sorted entry
    extract data
    return result
close sorter

This approach works, but it comes with a cost.

  • Additional memory usage
  • Extra processing for sorting
  • Potential performance overhead for large datasets

That is why SQLite always tries to use an index for ORDER BY whenever possible.

Why Index Selection Is So Important

At this stage, multiple layers of optimization come together:

  • WHERE clause determines candidate indexes
  • Join ordering decides when a table is accessed
  • Index selection decides how it is accessed
  • ORDER BY may influence which index is preferred

A single decision here can affect:

  • Number of rows scanned
  • Need for sorting
  • Overall execution time

Wrapping Up

Index selection is one of the most critical decisions in SQLite’s optimizer.

Even when multiple indexes are available, SQLite carefully evaluates which one minimizes total work.

It also balances filtering efficiency with sorting requirements to produce the fastest possible execution plan.

Understanding how SQLite makes this decision helps you design better indexes and write queries that align with the optimizer.

In the next part, we will explore GROUP BY and MIN/MAX optimizations, where SQLite applies additional strategies to aggregate and summarize data efficiently.

git-lrc
*AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo

HexmosTech
/
git-lrc

Free, Unlimited AI Code Reviews That Run on Commit

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud
operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won’t notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a…

I Built an AI That Recommends Therapy Pets — Here’s What I Learned (and the Code)

We’ve all heard about therapy dogs. But what if you’re allergic? Or live in a small apartment? Or just… really vibe with guinea pigs?

That was the problem I set out to solve. The result was an AI-powered pet therapy matching system. Here’s the full tech breakdown — and a few things that surprised me.

The Core Problem

Pet therapy is genuinely effective for anxiety, loneliness, and depression. Studies back it up. But “just get a dog” is terrible advice for:

  • Apartment dwellers
  • People with allergies
  • Those with limited mobility
  • Anyone who’s never owned a pet and has zero idea what they’re getting into

The matching problem is actually fascinating from an ML perspective: you’re trying to align human emotional needs, lifestyle constraints, and animal behavioral profiles — three completely different feature spaces.

The Architecture

User Input (survey)
        ↓
  Feature Extraction
        ↓
  Embedding Layer (sentence-transformers)
        ↓
  Similarity Scoring against Animal Profiles
        ↓
  Ranked Recommendations + Explanations
        ↓
  LLM-generated personalized summary

Simple. But the devil is in each layer.

Step 1: The Survey → Feature Vector

We don’t ask “what pet do you want?” (everyone says dog). We ask behavioral and lifestyle questions:

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

def encode_user_profile(survey_responses: dict) -> np.ndarray:
    """
    Convert free-text + structured survey answers into a dense vector.
    """
    # Combine all responses into a natural-language description
    profile_text = f"""
    Activity level: {survey_responses['activity_level']}.
    Living situation: {survey_responses['living_situation']}.
    Primary emotional need: {survey_responses['emotional_need']}.
    Time available daily: {survey_responses['daily_time_hours']} hours.
    Experience with animals: {survey_responses['experience']}.
    Allergies or restrictions: {survey_responses.get('restrictions', 'none')}.
    """

    return model.encode(profile_text)

# Example
user = encode_user_profile({
    "activity_level": "low, mostly sedentary",
    "living_situation": "small apartment, no yard",
    "emotional_need": "companionship and stress relief",
    "daily_time_hours": 1.5,
    "experience": "never owned a pet",
    "restrictions": "mild cat allergy"
})

Why sentence-transformers over a rule-based system? Because humans describe themselves weirdly. “I work from home 80% of the time” and “I’m home most days” should map to the same feature space. Embeddings handle this naturally.

Step 2: The Animal Profile Database

This is where we encode animal behavioral data the same way:

animal_profiles = [
    {
        "id": "rabbit_holland_lop",
        "name": "Holland Lop Rabbit",
        "description": """
        Quiet, affectionate, and apartment-friendly. Doesn't require outdoor walks.
        Hypoallergenic option for mild allergy sufferers. Responds well to gentle 
        handling and routine. Ideal for calm, indoor environments. Moderate time 
        commitment — about 1-2 hours of interaction daily is ideal.
        """
    },
    {
        "id": "guinea_pig_pair",
        "name": "Guinea Pig (bonded pair)",
        "description": """
        Social, vocal, low-maintenance. Perfect for small spaces. No allergy concerns 
        for most people. Highly responsive to their owners — will 'wheek' when they 
        hear you. Great for emotional support without high physical demands.
        """
    },
    # ... more profiles
]

# Pre-compute embeddings for all animal profiles
animal_vectors = {
    p["id"]: model.encode(p["description"])
    for p in animal_profiles
}

Step 3: Cosine Similarity Scoring

from sklearn.metrics.pairwise import cosine_similarity

def rank_recommendations(user_vector: np.ndarray, animal_vectors: dict) -> list:
    scores = []

    for animal_id, animal_vec in animal_vectors.items():
        sim = cosine_similarity(
            user_vector.reshape(1, -1),
            animal_vec.reshape(1, -1)
        )[0][0]
        scores.append((animal_id, float(sim)))

    # Sort by similarity descending
    return sorted(scores, key=lambda x: x[1], reverse=True)

top_matches = rank_recommendations(user, animal_vectors)
# → [("guinea_pig_pair", 0.87), ("rabbit_holland_lop", 0.82), ...]

Step 4: LLM-Generated Explanations

The raw similarity score means nothing to a user. We pass the top matches to an LLM to generate a warm, personalized explanation:

import anthropic

client = anthropic.Anthropic()

def generate_explanation(user_survey: dict, top_match: dict) -> str:
    prompt = f"""
    A person with the following profile:
    {user_survey}

    Has been matched with: {top_match['name']}
    Match reason (technical): {top_match['description']}

    Write a warm, 2-3 sentence explanation of WHY this pet is a great match for them.
    Be specific to their situation. Avoid generic phrases.
    """

    message = client.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )

    return message.content[0].text

This is the piece that makes users go “wow, it really gets me” — even though the underlying logic is just cosine similarity on text embeddings.

What Surprised Me

1. The “experience” feature matters more than I expected.

First-time pet owners consistently matched poorly with high-maintenance animals even when lifestyle fit was good. I had to add a soft penalty for mismatched experience levels.

2. Allergy handling is genuinely hard.

Allergy severity is non-linear. A “mild” cat allergy might be fine with a Siberian cat (lower Fel d 1 protein) but terrible with a Persian. I ended up adding a separate allergy-screening layer before the main matching.

3. Users lie on surveys (unconsciously).

People say they have 2 hours/day but actually mean 30 minutes. The solution? Add a calibration question: “How many hours do you spend on [comparable hobby] per week?” Cross-referencing revealed the actual available time.

Results After 3 Months

  • 78% of matched users reported being “very satisfied” with their recommendation at 30-day follow-up
  • Average session length: 4.2 minutes (users actually engage with the explanations)
  • Most surprising top match: Axolotls. Who knew they’d be such a good anxiety companion for people who “just want to watch something calming”?

Try It Live

If you want to see this in action — or if you’re genuinely curious what your ideal therapy animal is — the live matching system is at mypettherapist.com. Free to use, no signup required.

The codebase uses Python + FastAPI on the backend with React on the front. Happy to share more implementation details in the comments.

TL;DR

  • Use sentence-transformers to embed both user profiles and animal behavioral descriptions into the same vector space
  • Cosine similarity does the matching heavy lifting
  • LLM-generated explanations are what actually make users trust the result
  • Allergy handling and experience calibration are underrated complexity sources

What’s the weirdest AI matching problem you’ve tackled? Drop it in the comments — genuinely curious.

GDPR for Ecommerce: Customer Orders, Abandoned Carts, and Retargeting

Running an online shop means processing personal data at every stage of the customer journey — from the first page view to the final returns label. GDPR applies to all of it.

This guide covers the highest-risk areas for ecommerce businesses: order and delivery data, abandoned cart emails, retargeting pixels, email marketing consent, payment processors, customer deletion rights, product reviews, returns and fraud detection, age verification, international transfers, and data breach response.

Order and Delivery Data

Every completed order generates a rich personal data record: name, email, delivery address, payment reference, and order history. The lawful basis is contractual necessity (Article 6(1)(b)). Keep order records for VAT/tax compliance periods (typically 6-7 years), then delete. Every fulfilment warehouse, courier, and logistics provider must have a Data Processing Agreement (DPA) in place.

Abandoned Cart Emails: Consent vs Legitimate Interest

Abandoned cart emails are one of the most contested GDPR areas for ecommerce. Some argue a single timely reminder falls within legitimate interests — but under PECR, sending marketing emails to individuals requires prior consent unless the soft opt-in exemption applies. A guest checkout with no marketing opt-in does not satisfy the soft opt-in. The safest route: collect a separate marketing opt-in at checkout and limit abandoned cart sequences to subscribers.

Retargeting Pixels: Meta, Google, and Cookie Consent

Running Facebook Pixel, Google Ads tags, or TikTok Pixel requires prior, explicit, informed consent before the pixels fire. Your cookie consent banner must block all advertising pixels until the user actively accepts them, allow category-by-category acceptance, and record consent with a timestamp. Do not pre-tick advertising consent or use dark patterns.

Email Marketing and Double Opt-In

Consent must be freely given, specific, informed, and unambiguous. Double opt-in is strongly recommended — it provides clear evidence of consent and helps demonstrate compliance. Maintain a consent record for every subscriber including timestamp, form version, and consent wording.

Payment Processors and Shipping Partners

Stripe and PayPal typically act as independent data controllers for their own fraud and compliance purposes — disclose this in your privacy policy. Couriers and fulfilment providers are your data processors and require DPAs. If they transfer data outside the UK/EEA, your DPA must include Standard Contractual Clauses.

Customer Deletion Rights

GDPR’s right to erasure means customers can request account deletion. Delete all data not required for legal retention purposes, retain order records for the required period, remove them from marketing lists immediately, and confirm deletion in writing. Make the process easy — the ICO has taken a dim view of organisations that make erasure deliberately difficult.

Fraud Detection and Automated Decision-Making

If you use automated fraud scoring to decline orders without human review, you may be engaging in automated decision-making under Article 22. Where decisions have a significant effect on individuals, you must inform customers, give them the right to human review, and explain the logic in your privacy policy.

International Transfers

Shipping internationally means transferring customer data to countries outside the UK/EEA. Ensure your DPAs with international fulfilment partners include adequate safeguards — typically Standard Contractual Clauses.

Data Breach Response

Notify the ICO within 72 hours of becoming aware of a breach posing risk to individuals. Notify affected customers where there is high risk to their rights and freedoms. Document every breach and your response, even if you decide not to notify.

Scan your ecommerce store for compliance gaps at https://app.custodia-privacy.com/scan — free, no signup required, results in 60 seconds.

Turning Weekly GitHub Activity Into Blog Posts on Notion + DEV.to

DevNotion pipeline Architecture

This is a submission for the Notion MCP Challenge

What I Built

Every Monday standup, someone asks: “What did you work on last week?” And every Monday, I stare at my screen trying to remember. Did I merge that PR on Wednesday or Thursday? Was that refactor in the auth module or the pipeline? How many repos did I even touch?

I got tired of that blank moment. So I built DevNotion — a 3-agent pipeline powered by Mastra that harvests my entire week of GitHub activity, narrates it into a first-person blog post using Gemini, and publishes it to Notion (as a planner-style page with structured tables) and DEV.to (as a draft article). Every Sunday, automatically, via GitHub Actions.

No more Monday amnesia. The blog writes itself.

What it actually does

  1. Harvests my GitHub activity via GraphQL — commits, PRs, issues, code reviews, discussions, language stats, contribution streak
  2. Narrates the raw data into a casual, first-person blog post using Gemini (with a deterministic fallback if the LLM is unavailable)
  3. Publishes to two platforms simultaneously:

    • Notion — a planner-style page with stats tables, repo breakdowns, PR/issue/review tables, language breakdown, and the full blog post
    • DEV.to — a draft article ready for review

Key features

  • 3 specialized agents — each does one thing well (harvest, narrate, publish)
  • LLM only where it adds value — harvest and publish are deterministic, zero token overhead
  • 4 blog tone profiles — casual (default), professional, technical, storytelling
  • Planner-style Notion pages — not just a wall of text, but structured tables with stats, repos, PRs, issues, reviews, and languages
  • Notion MCP integration — full Notion API surface via Model Context Protocol
  • Notion Markdown Content API — write rich markdown directly to pages (the real game changer)
  • DEV.to draft publishing — articles created as drafts, ready to review and publish
  • GitHub Actions CI — weekly cron (Sundays 08:00 UTC) + manual dispatch
  • Blog log in README — CI auto-commits a metrics table after each run
  • Fallback chain — always produces a blog post, even if Gemini is down
  • Rate limiting everywhere — p-queue + p-retry for both Notion and DEV.to APIs

Architecture

DevNotion pipeline Architecture

Step Agent What it does
Harvest github-harvest-agent Fetches weekly GitHub data via GraphQL (deterministic)
Narrate narrator-agent Writes a first-person blog post from the data
Publish publisher-agent Creates Notion planner page + DEV.to draft via direct APIs

The pipeline only uses an LLM where it genuinely adds value — narration. Harvest and publish are pure function calls. No token overhead, no hallucination risk, faster execution.

Architecture Deep-Dive

Why 3 agents, not 1?

I could’ve built one mega-agent that does everything. But that’s a recipe for:

  • Burning tokens on deterministic work (fetching GitHub data doesn’t need an LLM)
  • Hallucinating URLs and stats (the publish step should never make things up)
  • Debugging nightmares (which part of the monolith failed?)

Instead, each agent is a specialist. The workflow chains them together:

export const weeklyDispatchWorkflow = createWorkflow({
  id: 'weekly-dispatch',
  inputSchema: z.object({ weekStart: z.string() }),
  outputSchema: PublishOutputSchema,
})
  .then(harvestStep)
  .then(narrateStep)
  .then(publishStep)
  .commit();

Three steps, chained with .then(), committed as a single workflow. Mastra handles the data handoff between steps automatically.

Harvest: deterministic data, zero LLM

The harvest step calls GitHub’s GraphQL API directly — no agent reasoning needed:

const harvestStep = createStep({
  id: 'harvest-github',
  inputSchema: z.object({ weekStart: z.string() }),
  outputSchema: WeeklyDataSchema,
  execute: async ({ inputData }) => {
    const data = await fetchWeeklyContributions(inputData.weekStart);
    return WeeklyDataSchema.parse(data);
  },
});

One GraphQL query pulls commits, PRs, issues, reviews, discussions, language stats, and contribution streak for the week. The response gets validated through a Zod schema. No LLM in the loop — this is pure data fetching.

Narrate: LLM with a fallback chain

This is where the LLM earns its keep. The narrator agent takes raw JSON and writes a blog post that sounds like I wrote it myself. The system prompt has a full personality profile — it writes in first person, knows my tech stack (Python, Rust, TypeScript), references my OSS work, and matches one of four tone profiles.

But LLMs can be flaky. So the narrate step has a fallback chain:

const narrateStep = createStep({
  id: 'narrate',
  execute: async ({ inputData, mastra }) => {
    const agent = mastra!.getAgent('narrator-agent');
    let blog;

    try {
      const result = await agent.generate(prompt);
      const parsed = parseFrontmatter(result.text);
      if (parsed.success) {
        blog = parsed.data.blog;
      } else {
        blog = buildFallbackNarration(inputData).blog;
      }
    } catch (err) {
      blog = buildFallbackNarration(inputData).blog;
    }

    return { blog, weeklyData: inputData };
  },
});
  1. Gemini generates a markdown blog with YAML frontmatter
  2. If parsing fails → deterministic fallback builds a basic post from raw data

A blog post is always produced, even if Gemini is completely down.

Publish: Notion planner + DEV.to draft

The publish step is where things get interesting. It doesn’t just dump text into Notion — it builds a planner-style page with structured tables:

const publishStep = createStep({
  id: 'publish',
  execute: async ({ inputData }) => {
    const { blog, weeklyData } = inputData;

    // 1. Create Notion page
    const createResult = await createNotionPage(title);

    // 2. Create DEV.to draft (so the link goes into the Notion planner)
    const devtoResult = await createDevtoArticle({
      title: blog.headline,
      body_markdown: buildDevtoMarkdown(blog),
      tags: blog.tags,
      published: false,
    });

    // 3. Write planner markdown to Notion (includes DEV.to link)
    const plannerMd = buildPlannerMarkdown(weeklyData, blog, links);
    await writeNotionMarkdown(notionPageId, plannerMd);
  },
});

The order matters: DEV.to draft gets created before writing the Notion page content, so the Notion planner can include a link to the DEV.to draft. Cross-platform linking, done right.

Each Notion page includes:

  • Published Links table — Notion page URL + DEV.to draft edit link
  • Week at a Glance — commits, PRs, issues, reviews, lines added/removed, streak
  • Active Repositories — repo name, commits, language, line changes
  • Pull Requests / Issues / Reviews / Discussions — structured tables
  • Languages — top languages by commit count
  • Full blog post — the narrated content below a separator

// Detect dark theme
var iframe = document.getElementById(‘tweet-2037594562424914027-412’);
if (document.body.className.includes(‘dark-theme’)) {
iframe.src = “https://platform.twitter.com/embed/Tweet.html?id=2037594562424914027&theme=dark”
}


DevNotion Project

Star DevNotion on GitHub

How I Used Notion MCP

This is the part I’m most excited about. DevNotion uses the Notion MCP Server in two complementary ways:

1. Notion MCP Server via @mastra/mcp

The publisher agent integrates with the official @notionhq/notion-mcp-server through Mastra’s MCP client. This gives the agent access to the full Notion API surface via Model Context Protocol:

import { MCPClient } from '@mastra/mcp';

export const notionMcp = new MCPClient({
  servers: {
    notion: {
      command: 'npx',
      args: ['-y', '@notionhq/notion-mcp-server'],
      env: {
        OPENAPI_MCP_HEADERS: JSON.stringify({
          Authorization: `Bearer ${env.NOTION_TOKEN}`,
          'Notion-Version': '2022-06-28',
        }),
      },
    },
  },
  timeout: 30000,
});

The MCP tools are loaded lazily with a graceful fallback — if the MCP server fails to start, the direct tools still work independently:

export async function getNotionMcpTools(): Promise<Record<string, any>> {
  try {
    return await notionMcp.listTools();
  } catch (err) {
    console.warn('MCP: Notion MCP server unavailable, using direct tools only');
    return {};
  }
}

2. Direct tools + MCP tools merged

The publisher agent merges both tool sets — MCP tools for the full Notion API surface, and direct tools for capabilities MCP doesn’t cover:

// Direct tools (Markdown Content API + DEV.to — not available via MCP)
const directTools = {
  createNotionPage: createNotionPageTool,
  writeMarkdown: writeMarkdownTool,
  searchNotion: searchNotionTool,
  updateNotionPage: updateNotionPageTool,
};

// Merge: Notion MCP tools + direct tools
const mcpTools = await getNotionMcpTools();
const tools = { ...mcpTools, ...directTools };

This dual approach means the publisher agent gets the best of both worlds — MCP’s broad API surface for interactive use in the Mastra playground, plus direct tools for the automated workflow.

3. The Markdown Content API (the game changer)

This is the Notion feature that made the planner-style pages possible. Instead of constructing Notion blocks one by one (which is painful and rate-limit-heavy), I write the entire page as markdown in one API call:

const response = await fetch(
  `https://api.notion.com/v1/pages/${pageId}/markdown`,
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${env.NOTION_TOKEN}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2026-03-11',
    },
    body: JSON.stringify({
      type: 'replace_content',
      replace_content: { new_str: markdown },
    }),
  },
);

One PATCH request replaces the entire page content with rich markdown — including tables, headings, blockquotes, links, code blocks, everything. This is what powers the planner-style layout with structured stats tables + the full blog post, all in a single API call.

4. Rate limiting

Notion’s API allows roughly 3 requests per second. Every Notion call (MCP and direct) goes through a shared rate limiter:

const queue = new PQueue({ concurrency: 1, interval: 334, intervalCap: 1 });

async function rateLimited<T>(fn: () => Promise<T>): Promise<T> {
  return queue.add(() => pRetry(fn, { retries: 3 })) as Promise<T>;
}

p-queue throttles concurrency, p-retry handles transient failures. I learned this the hard way — without rate limiting, the Notion API will 429 you into oblivion when you’re creating a page, writing markdown, and updating the icon in quick succession.

Lessons Learned

Rate limits are the real boss

Notion (3 req/s), DEV.to (30 req/30s), GitHub GraphQL (5000 points/hr) — every API has its own throttle. I ended up with p-queue + p-retry wrappers around everything. The rate limiter code is almost identical across all three services, and honestly, it should probably be a shared utility. But three similar lines of code is better than a premature abstraction.

Structured output is slower than you’d think

I originally used Gemini’s native JSON schema for structured output (agent.generate(prompt, { structuredOutput: { schema } })). It worked, but added 20-40 seconds per call. Switching to plain text generation with YAML frontmatter parsing was 3-4x faster and just as reliable. The deterministic fallback catches the rare parsing failure.

Gemini model musical chairs

I’ve been through three Gemini models on this project: gemini-2.5-flash-preview-04-17 (retired), gemini-2.5-flash (stable but slow for structured output), and now gemini-3-flash-preview (current). The lesson: always make the model configurable via env vars. Hardcoding model IDs is a recipe for broken deploys.

The Zod conflict that broke everything

Mastra and my code both depend on Zod, but different versions. Having two Zod instances means z.string() from one isn’t recognized by the other — schema validation just silently fails. The fix: a single line in package.json:

{
  "pnpm": {
    "overrides": {
      "zod": "$zod"
    }
  }
}

Forces pnpm to deduplicate to one Zod version. Took me way too long to figure that out.

Direct API calls beat agent reasoning for deterministic work

The harvest and publish steps started as full agent calls. But an LLM doesn’t add anything when the task is “call this GraphQL endpoint and return the result.” Switching to direct function calls made the pipeline faster, cheaper, and more predictable. Only use an LLM where you need creativity or reasoning — everywhere else, just write a function.

Built with Mastra, Gemini, Notion API, and a lot of coffee. If you’ve ever forgotten what you worked on last week, give DevNotion a try.

CreatorConfig: PCPartPicker for streaming & podcast gear

CreatorConfig helps you build a streaming, podcasting, or content creation setup that actually works before you buy. Get compatibility checking, setup warnings, and complete gear lists with one click.

The Stack

  • Frontend: React + TypeScript + Vite
  • Styling/UI: Tailwind CSS + custom component patterns
  • Backend: Node.js (ESM) + Express-style API routes
  • Database: PostgreSQL

The Product

CreatorConfig is a creator gear setup platform where users can build, save, and share complete streaming/podcasting/content-creation setups.

Core product pillars:

  • Builder-first workflow: pick parts by category (mic, camera, interface, mixer, etc.) in a structured flow.
  • Setup publishing system: keep setups private, publish publicly, or feature curated completed setups.
  • Setup detail pages: each setup has its own URL, story content, product breakdown, and buy links.
  • Profile ecosystem: users can manage setups, saved items, and activity from account/profile pages.
  • Discovery layer: browse parts, completed setups, and community builds with cleaner SEO-friendly routes.

Goal: make choosing creator gear less guesswork and more systemized.