Building a P2P ‘Wikipedia for Machines’: Verifiable RAG with the Holepunch Stack

Current LLMs hallucinate when they lack context. While Retrieval-Augmented Generation (RAG) helps, existing pipelines force a tough compromise: you either trust centralized search APIs, scrape the live web (which is slow, fragile, and bloated with SEO spam), or maintain heavy, complex crawling infrastructure yourself.

More importantly, none of these methods give you cryptographic proof that the content your LLM is citing actually came from the source you claim.

To solve this, I’ve been building HIVE—a decentralized, peer-to-peer knowledge base designed to be consumed by LLMs, not humans. Think of it as a “Wikipedia for machines” that is completely P2P, cryptographically verifiable, and has no central authority.

The Architecture: BEEs and Queens
Instead of building a monolithic crawler, HIVE splits the workload into two distinct peer topologies using a pure Holepunch stack (Hypercore + Hyperswarm). There is no shared coordination ledger or Autobase; it relies entirely on single-writer logs.

[ Wikipedia / arXiv / RSS ]
│
▼ (Autonomous Extraction & Signing)
┌─────────┐
│   BEE   │ (Producer Node - Low Power)
└────┬────┘
│
▼ (Hyperswarm DHT / P2P Replication)
┌─────────┐
│  QUEEN  │ (Consumer Node - Heavy Indexer)
└────┬────┘
│
▼ (Semantic Vector Search)
[ Qdrant ] ──► [ Local LLM / API Synthesis ]
  1. BEEs (Producer Nodes)
    BEEs are lightweight, Raspberry Pi-friendly nodes. They autonomously crawl and extract knowledge from structured sources (currently heavy on Wikipedia, with initial arXiv and RSS support).

Every extracted text fragment is ed25519-signed by the BEE.

The signed fragments are appended to the node’s local, single-writer Hypercore feed.

Other nodes can only replicate this feed as read-only.

  1. Queens (Consumer Nodes)
    Queens are the heavy indexer and query nodes.

They discover active BEEs via the Hyperswarm DHT.

They replicate all discovery feeds entirely over P2P—there is zero HTTP traffic between nodes.

As they ingest the data, they verify the ed25519 signatures to guarantee data integrity.

The fragments are then indexed into a local Qdrant vector database to serve semantic queries.

When an LLM queries a Queen, it receives signed, source-traceable fragments with absolute cryptographic proof of origin.

Try It In 3 Minutes (Docker Compose)
If you want to spin up the full stack locally—including a BEE node, a Queen indexer, Qdrant, and a reverse proxy—you can run it via Docker:

git clone https://github.com/capybarist/hive.git && cd hive
cp .env.example .env
nano .env  # Paste your LLM API Key (e.g., a free Gemini key from aistudio.google.com)
docker compose up -d

Queen Query Endpoint: Open http://localhost to test the AI synthesis.

BEE Dashboard: Open http://localhost:8080 to monitor autonomous extraction activity.

Minimal Path (~150MB)
If you just want to run a lightweight BEE node to contribute to the network without a heavy vector indexing endpoint:

git clone https://github.com/capybarist/hive.git && cd hive
npm install
echo "LLM_PROVIDER=gemini" > .env
echo "LLM_API_KEY=your_key" >> .env
bash hive.sh

Current State & Honest Limitations (v0.7.0)
HIVE is functional, but it is not battle-tested at a massive scale yet. Here is exactly where the project stands today:

What Works: Full Bee/Queen role splitting, native Hypercore replication over Hyperswarm DHT, automated signature verification on block receipt, and a working Wikipedia forager with a persistent BFS crawl queue.

The Bottlenecks: The extraction rate is highly dependent on your local hardware or your LLM provider’s rate limits (e.g., Groq free tier yields ~6k fragments/day, while an Ollama CPU setup does around ~600/day). Additionally, Hyperswarm DHT can still be finicky behind aggressive corporate firewalls.

Roadmap: What’s Next (v0.7.x)
Source-Driven Extraction: Transitioning from a static topic tree to per-BEE source declarations (Wikipedia, arXiv, Common Crawl).

Unified ForagerSource Interface: Making it so adding a new data source only requires implementing a single interface file.

Common Crawl Integration: Indexing the open web via reproducible, immutable snapshots instead of live scraping.

Score-by-Corroboration: Implementing an algorithm to boost content confidence when multiple independent BEEs extract identical facts from different sources.

Looking for Feedback
HIVE is completely open-source (BUSL, automatically converting to MIT in 4 years). There is no startup behind this, no VC funding, and absolutely no token economy or cryptocurrency attached. I built it because I needed verifiable local RAG and couldn’t find an existing solution.

I would love to hear from developers working on P2P architectures, distributed systems, or self-hosted RAG:

The Topo Split: Does the Bee/Queen separation feel sound to you, or do you see architectural edge cases that could lead to unexpected bottlenecks?

Open Web Ingestion: Is leveraging Common Crawl snapshots a solid approach for reproducible web ingestion, or are the local storage overheads too punishing for self-hosted nodes?

The P2P Commons: Would you dedicate spare home-lab compute or bandwidth to seed/produce signed knowledge fragments for a shared network?

Let me know your thoughts or hit me with some tough architectural critique in the comments!

📂 Source Code: https://github.com/capybarist/hive

📖 Manifesto: https://github.com/capybarist/hive/blob/main/MANIFESTO.md

🏗️ Technical Deep-Dive: https://github.com/capybarist/hive/blob/main/CLAUDE.md

One Command, One Working Kubernetes Cluster! Building My Daily-Driver Lab on OrbStack.

OrbStack native K8s up & running — one command, instant cluster.<br>
Starting the cluster

Part 2 of 7 — The Mac Kubernetes Lab: A Production-Mirror Setup from Scratch.

Previously in Part 1: I walked through why I replaced Multipass with OrbStack, the dual-cluster architecture I settled on, and a preview of the M1 vs M4 CNI problem that’s coming in Part 4.

The cluster I am going to set up in this article is the one I spend most of my working day inside. It’s a single-node Kubernetes cluster, always on, idles at around 512 MB of memory, has real LoadBalancer IPs and wildcard DNS out of the box. No MetalLB, /etc/hosts editing,kubectl port-forward muscle memory. By the time this article is done, it will also have Istio, Vault, and Crossplane running. Total elapsed time, the first time you do it: about ten minutes.

If you’ve ever built a local Kubernetes cluster and then spent the next twenty minutes wiring up MetalLB and editing /etc/hosts so you can actually reach a service from a browser, this is going to feel almost suspicious.

OrbStack native K8s up & running — one command, instant cluster.<br>
Starting the cluster

# 💻 Mac
orb start k8s

# Confirm cluster is udr
kubectl get nodes
# NAME       STATUS   ROLES                  AGE   VERSION
# orbstack   Ready    control-plane,master   30s   v1.33.x

kubectl config current-context
# orbstack

That’s all! No kubeadm, no CNI configuration, no certificate management. The cluster is up and reachable in under thirty seconds the first time, and instantly on every subsequent start.

What makes OrbStack’s networking special?

This is where OrbStack genuinely earns its keep. On a typical local cluster; kind, minikube, kubeadm, LoadBalancer services stay in pending state until you install MetalLB on OrbStack:

  • LoadBalancer services get a real, reachable IP automatically.
  • *.k8s.orb.local wildcard DNS resolves from your Mac browser, with no /etc/hosts entry.
  • cluster.local DNS also resolves from your Mac.
  • Every service type is reachable without kubectl port-forward.

⚠️ The wildcard DNS only resolves on your Mac. Other devices on your network won’t see *.k8s.orb.local. If you need a service reachable from another machine, that’s what Cluster 2 (Parts 3–6) is for.

Installing Istio via Helm.

I use Helm rather than istioctl for two reasons.
First, it’s how I manage Istio on the production EKS clusters at work, so the muscle memory transfers.
Second, Helm gives fine-grained control over resource requests, which matters on a laptop.

Istio components — istiod control plane, ingress gateway, and sidecar proxies

# 💻 Mac
kubectx orbstack

# Add helm charts
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update

# Step 1 - Base CRDs
helm install istio-base istio/base 
  --namespace istio-system --create-namespace 
  --set defaultRevision=default

# Step 2 - Control plane
# The PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION flag is required on OrbStack
# to prevent DNS resolution conflicts with the host network.
helm install istiod istio/istiod 
  --namespace istio-system 
  --set pilot.env.PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION=true 
  --set global.proxy.resources.requests.cpu=10m 
  --set global.proxy.resources.requests.memory=64Mi 
  --wait

# Step 3 - Ingress gateway
# OrbStack assigns a real LoadBalancer IP automatically - no MetalLB needed.
helm install istio-ingress istio/gateway 
  --namespace istio-ingress --create-namespace 
  --set service.type=LoadBalancer

# Verify the gateway got an EXTERNAL-IP
kubectl get svc -n istio-ingress
# NAME            TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)
# istio-ingress   LoadBalancer   10.x.x.x     198.19.x.x    80:xxx/TCP

# Enable sidecar injection on the default namespace
kubectl label namespace default istio-injection=enabled

Gateways and Virtual Services on the native cluster.

This is the moment OrbStack feels like cheating. You create a Gateway pointing at *.k8s.orb.local, and it just works from your Mac browser. No IP lookups. No /etc/hosts, no 127.0.0.1:8080 proxying.

How the traffic actually flows.

The Gateway resource binds to the istio-ingress LoadBalancer service, OrbStack intercepts traffic to the *.k8s.orb.localwildcard domain at the Mac level and routes it to that LoadBalancer IP. The Virtual Service then routes inside the cluster to the right service.

Traffic flow from Mac browser through OrbStack wildcard DNS to a pod

Example 1: Basic routing with httpbin:

# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      containers:
      - name: httpbin
        image: kennethreitz/httpbin:latest
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  selector:
    app: httpbin
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingress
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "httpbin.k8s.orb.local"
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.k8s.orb.local"
  gateways:
  - httpbin-gateway
  http:
  - route:
    - destination:
        host: httpbin
        port:
          number: 80
EOF

Open http://httpbin.k8s.orb.local in your Mac browser. It works immediately.
httpbin screenshot

Example 2: Traffic splitting (canary):

The same pattern used in production canary deployments:

# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: myapp-split
spec:
  hosts:
  - "myapp.k8s.orb.local"
  gateways:
  - myapp-gateway
  http:
  - route:
    - destination:
        host: myapp-v1
        port:
          number: 80
      weight: 80
    - destination:
        host: myapp-v2
        port:
          number: 80
      weight: 20
EOF

Example 3: Path-based routing:

Route /v1 and /v2 to different backend services:

# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: path-routing
spec:
  hosts:
  - "api.k8s.orb.local"
  gateways:
  - api-gateway
  http:
  - match:
    - uri:
        prefix: /v1
    route:
    - destination:
        host: api-v1-svc
        port:
          number: 80
  - match:
    - uri:
        prefix: /v2
    route:
    - destination:
        host: api-v2-svc
        port:
          number: 80
EOF

One OrbStack gotcha: don’t enable httpsRedirect!

Do not use httpsRedirect: true in a Gateway on the native cluster. OrbStack intercepts LoadBalancer traffic in a way that causes an infinite 301 redirect loop when TLS redirect is enabled.

# ❌ This breaks on OrbStack native K8s
servers:
- tls:
    httpsRedirect: true

# ✅ Use plain HTTP on the native cluster
servers:
- port:
    number: 80
    protocol: HTTP

For TLS testing, use Cluster 2 (the VM cluster, coming in Part 3), where you have full control over the network stack.

Installing the rest of the daily stack:

Vault in dev mode.

Dev mode trades durability for speed. No unsealing, no persistence concerns, instant startup. It’s the right call for the daily-driver cluster where I’m testing AppRole workflows, PKI policies, or Kubernetes auth configurations, and the value is in iteration speed, not data preservation.

# 💻 Mac
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

helm install vault hashicorp/vault 
  --namespace vault --create-namespace 
  --set "server.dev.enabled=true"

kubectl get pods -n vault
# vault-0   1/1   Running   0   30s

For production-grade Vault with HA Raft storage, use Cluster 2 (Part 3). Dev mode here is intentional — it trades durability for speed.

Crossplane:

Crossplane turns the Kubernetes cluster into a universal control plane for cloud infrastructure. I use it heavily with the AWS provider at work.

# 💻 Mac
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

# Note: --enable-composition-functions was removed in newer versions.
# Composition Functions are enabled by default now.
helm install crossplane crossplane-stable/crossplane 
  --namespace crossplane-system --create-namespace

kubectl get pods -n crossplane-system

Stop/start without losing your work.

# 💻 Mac

# Stop - releases all CPU and RAM
orb stop k8s

# Start - full state persists (deployments, services, secrets, configmaps)
orb start k8s

# Verify
kubectx orbstack
kubectl get nodes
kubectl get pods -A

The native cluster state persists across stop/start. Vault dev mode data is lost on restart by design, but everything else, Crossplane installations, Istio configuration, and your workload deployments come back exactly as provisioned.

Quick reference for this cluster:

Quick reference for this cluster.

What’s next.

The daily-driver cluster is the easy half of this lab. The hard half is the one that mirrors production, a real multi-node kubeadm cluster running on four VMs, with HashiCorp Vault as its certificate authority.

That’s where Part 3 picks up: creating the VMs, wiring their networking, and standing up a 3-tier Vault PKI that will sign every certificate in the cluster.

← Part 1: Why I Replaced Multipass with OrbStack.. | Part 3: Building a Production-Grade Vault PKI for a Local kubeadm Cluster Without the Shortcuts →

I’m Noah Makau, a DevSecOps engineer based in Nairobi. I run a small DevOps consultancy and hold CKA, CKAD, and AWS Solutions Architect Professional certifications, currently preparing for CKS. I write about Kubernetes, Vault, Crossplane, and the day-to-day of running platforms that actually have to stay up.

Originally published at blog.arkilasystems.com.

npm Supply Chain Audit: The Checklist Most Teams Stop Too Early

Originally posted on getcommit.dev.

In October 2021, ua-parser-js was used by Facebook, Microsoft, Amazon, and Google. It had 7 million weekly downloads. It had no reported CVEs. It had clean code and an active maintainer. Every security tool in the npm ecosystem reported: nothing wrong here.

Then the maintainer’s npm token was compromised. A malicious release deployed a cryptominer and credential stealer to every CI pipeline and production server that ran npm install that day. The blast radius: four hours, millions of installs, Fortune 500 pipelines.

The same structural profile — single maintainer, massive download volume, clean code — was present before the attack. It was visible, computable from public data, and nobody was measuring it.

This is the gap most npm supply chain audits leave open.

The three-layer model

A complete npm supply chain audit covers three distinct layers. Most teams run one. Sophisticated teams run two. Almost nobody runs all three — because the third layer only became a standard practice after real-world attacks validated the signal.

Layer Question answered Tools Timing
1 — Known vulnerabilities Does this version have a documented CVE? npm audit, Snyk After discovery
2 — Code-level anomalies Is this package doing something suspicious right now? Socket At publish time
3 — Structural risk Is this package a high-value attack target before any attack occurs? proof-of-commitment Before the event

Each layer is essential. Each one fails at what the others cover. A supply chain audit that uses only one or two is a partial audit.

Layer 1: Known vulnerability scanning

What it does

npm audit submits your dependency tree to GitHub’s Advisory Database and returns matches. Snyk extends this with a proprietary database, license compliance checking, and auto-fix pull requests. Both tools answer the same question: does this package version have a documented, reported vulnerability?

What it catches

Prototype pollution vulnerabilities. Known RCE bugs. Packages with published CVEs where the fix is a version bump. The entire category of “known bad” code.

What it misses

Anything that isn’t documented yet. A CVE requires discovery, analysis, and reporting — that process takes days to weeks after an attack. For the ua-parser-js attack, npm audit returned zero for the four hours the malicious version was live, and returned zero for every day before that, including the years when the structural risk was building.

Checklist: Layer 1

  • Run npm audit in CI on every push. Fail on high/critical.
  • Set a .npmrc audit-level threshold appropriate for your risk tolerance.
  • If using Snyk: enable the GitHub integration for automated PRs on new CVEs.
  • Review npm audit --json output for transitive vulnerabilities, not just direct dependencies.
  • Don’t suppress audit failures with npm audit --production if devDependencies run in CI pipelines.

Layer 2: Real-time code analysis

What it does

Socket performs static analysis on the actual published package source — not the CVE database, not the repository. When a new version publishes to the npm registry, Socket analyzes the code for suspicious patterns: dynamic eval of user-controlled strings, network calls to unexpected endpoints, obfuscated payloads, environment variable harvesting.

For the class of attack where a maintainer’s account is compromised and they push a malicious release, Socket catches the payload within minutes of publication — before most CI pipelines would run.

What it catches

Token-theft attacks. Malicious versions with obfuscated payloads. Packages that suddenly start making network calls they didn’t make before. The ua-parser-js attack, run through Socket today, would be flagged by the suspicious network activity and eval patterns in the malicious release.

What it misses

The structural risk that exists before any malicious code is published. Socket analyzes code. The code was clean until the attack happened. Socket correctly reports “nothing suspicious in the code” when the code is genuinely clean — it can’t report on structural conditions that live outside the code.

Socket also doesn’t score blast radius. A solo-maintained package with 400 million weekly downloads gets the same analysis as a solo-maintained package with 400 downloads. The structural risk is orders of magnitude different, and that difference isn’t in the code.

Checklist: Layer 2

  • Enable Socket on your GitHub organization. The free tier covers open source.
  • Configure Socket to block PRs that introduce packages with medium/high risk signals.
  • Subscribe to Socket alerts for packages already in your lockfile.
  • Review Socket’s “install scripts” and “network access” flags — these are rarely false positives for utility packages.

Layer 3: Structural risk scoring

What it does

Structural risk scoring answers the question that neither Layer 1 nor Layer 2 can answer: is this package a high-value attack target, right now, based on observable structural conditions?

The structural conditions that define a high-value npm target are three things:

  • Single publish credential. One npm account with publish access. One compromised token = one malicious release to every system that runs npm install.
  • High download volume. The blast radius of a compromised publish. Hundreds of millions of weekly installs means CI pipelines and production deployments at Fortune 500 companies.
  • Active maintenance. A package that publishes regularly is more valuable to an attacker than an abandoned one, because developers trust it and update to new versions.

All three of these signals are computable from public data. The npm registry exposes maintainer count per package. The npm download API returns weekly statistics. GitHub exposes commit history and contributor count. No scanning required. No proprietary database. The data was always there.

What it catches

The conditions that make a package a rational attack target before any attack has occurred. Event-stream (2018), ua-parser-js (2021), and colors.js (2022) all shared this structural profile: sole publisher, enormous download volume, active releases. The structural risk was computable and present for years before each incident. No existing tool was measuring it.

What it misses

Everything that requires code inspection. Structural scoring tells you about the conditions for an attack, not the attack itself. It generates leading indicators, not proof of compromise. A CRITICAL structural score means “this package is a high-value target” — not “this package is currently compromised.” For that, you need Layer 2.

Checklist: Layer 3

  • Run npx proof-of-commitment --file package-lock.json against your dependency tree. Note every CRITICAL flag (single maintainer, >10M weekly downloads).
  • For every CRITICAL package, document the risk in your threat model. Do you have a contingency if this package is compromised? Can you pin to a specific version and monitor for unexpected updates?
  • Check Trusted Publishing status for critical dependencies. Packages that publish via OIDC provenance (Trusted Publishing) are meaningfully harder to compromise than packages that publish via personal tokens.
  • Re-run quarterly. Package maintainer counts change when projects gain or lose contributors. A package that was safe at multi-maintainer status may drift to single-maintainer over time.
  • Evaluate transitive dependencies, not just direct ones. Your direct dependencies may look clean; the packages they depend on may carry CRITICAL flags. The blast radius includes the entire install tree.

The complete audit checklist

Run this in sequence. Each layer surfaces a different class of problem.

Before a major dependency update

# Layer 1: Known vulnerabilities
npm audit --json | jq '.vulnerabilities | length'

# Layer 2: Socket analysis (if CLI installed)
npx @socketsecurity/cli check package.json

# Layer 3: Structural risk
npx proof-of-commitment --file package-lock.json

In CI (on every PR)

# Layer 1
npm audit --audit-level=high

# Layer 3 (structural risk, fail on CRITICAL)
npx proof-of-commitment --file package-lock.json --fail-on-critical

Quarterly review

  • Re-run Layer 3 on the full dependency tree. Note any packages that gained CRITICAL status since last quarter.
  • Review Socket alert history for your organization. Note patterns.
  • Check Trusted Publishing adoption for your five most critical dependencies.
  • Update your threat model with any new structural risks.

Why most audits stop at Layer 1 or 2

The argument against Layer 3 is: “It’s a leading indicator, not an exploit. A CRITICAL flag doesn’t mean anything was attacked.”

That’s true. It’s also the argument for credit scoring before fraud detection exists. Leading indicators measure structural conditions that predict future events. They’re not proof of the event — they’re the reason you insure against it before it happens.

The practical objection is noise: if your audit returns CRITICAL flags for minimatch, @types/node, and lodash, what do you do with that? You can’t replace these packages. You can’t wait for them to gain more maintainers.

The answer isn’t to fix the packages. It’s to know the risk exists, document it in your threat model, and make informed decisions about dependency pinning, update monitoring, and response plans. The same way you know that your RDS instance is a single point of failure and set up a read replica anyway.

The ua-parser-js compromise lasted four hours and affected millions of systems because the teams running those systems didn’t have a response plan. They didn’t have one because nobody told them the risk existed. Layer 3 is the tool that tells you the risk exists.

Try the structural layer

proof-of-commitment is open source and zero-install:

# Scan your project
npx proof-of-commitment --file package-lock.json

# Scan specific packages
npx proof-of-commitment axios lodash chalk zod

# pnpm or yarn
npx proof-of-commitment --file pnpm-lock.yaml
npx proof-of-commitment --file yarn.lock

Or use the web interface at getcommit.dev/audit — paste a GitHub repo URL and get structural risk scores for every dependency in seconds.

Layer 1 tells you what broke. Layer 2 tells you what’s breaking now. Layer 3 tells you what will break. You need all three.

Get a daily business report in your inbox every morning — automate it with n8n (free workflow JSON)

Every founder has some version of a morning ritual: open 4 different tabs, check Stripe, check analytics, check the spreadsheet, try to mentally add up whether yesterday was a good day.

That’s 15 minutes every morning. 250+ times a year. 60+ hours you’ll never get back.

Here’s a 5-node n8n workflow that compiles your daily business metrics and delivers a clean formatted report to your inbox (and Slack) at 7 AM — automatically, every single day.

What the workflow does

Node 1 — Daily schedule trigger

Fires every morning at a configurable time. No polling, no manual runs. Set it once, forget it forever.

Node 2 — Google Sheets: read your metrics

Reads your business data from a Google Sheet — sales rows, revenue, order counts. The default expects columns Date, Revenue, Orders but you can reshape the Code node to match whatever you already track.

Node 3 — Code node: calculate KPIs

Processes the raw rows and computes:

  • Total revenue for the period
  • Order count
  • Average order value
  • Week-over-week change (yesterday vs same day last week)

The math is plain JavaScript — readable and easy to adjust.

Node 4 — Gmail: send the HTML report

Sends a clean, formatted HTML email with metric cards. Revenue in green, orders in blue, average order in purple, week-on-week trend in orange. You open your email and immediately know if yesterday was good or bad.

Node 5 — Slack: post the summary

Posts a concise text summary to a Slack webhook. Optional — just delete this node if you don’t use Slack.

Full workflow JSON

{
  "name": "Daily Business Report Generator",
  "nodes": [
    {"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 7 * * *"}]}},"id":"dr1","name":"Every Day at 7 AM","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[240,300]},
    {"parameters":{"documentId":{"__rl":true,"value":"YOUR_SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"Sales","mode":"name"},"options":{"rangeDefinition":"specifyRangeA1","range":"A:E"}},"id":"dr2","name":"Read Sales Data","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[460,300]},
    {"parameters":{"jsCode":"const rows = $input.all();nconst today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });nconst sales = rows.map(r => parseFloat(r.json.Revenue || 0));nconst totalRevenue = sales.reduce((a, b) => a + b, 0);nconst orderCount = rows.filter(r => r.json.Revenue && parseFloat(r.json.Revenue) > 0).length;nconst avgOrderValue = orderCount > 0 ? (totalRevenue / orderCount).toFixed(2) : 0;nconst yesterday = parseFloat(rows.slice(-1)[0]?.json?.Revenue || 0);nconst weekAgo = parseFloat(rows.slice(-8, -7)[0]?.json?.Revenue || 0);nconst weekChange = weekAgo > 0 ? (((yesterday - weekAgo) / weekAgo) * 100).toFixed(1) : 'N/A';nreturn [{ json: { date: today, totalRevenue: totalRevenue.toFixed(2), orderCount, avgOrderValue, weekOverWeekChange: weekChange } }];"},"id":"dr3","name":"Calculate KPIs","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300]},
    {"parameters":{"sendTo":"[email protected]","subject":"={{ '📊 Daily Report — ' + $json.date }}","emailType":"html","message":"=<html><body style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px'><h1 style='color:#2c3e50'>📊 Daily Business Report</h1><p style='color:#7f8c8d'>{{ $json.date }}</p><table width='100%' cellpadding='12'><tr><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#2ecc71'>${{ $json.totalRevenue }}</div><div style='color:#7f8c8d;font-size:13px'>Revenue</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#3498db'>{{ $json.orderCount }}</div><div style='color:#7f8c8d;font-size:13px'>Orders</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#9b59b6'>${{ $json.avgOrderValue }}</div><div style='color:#7f8c8d;font-size:13px'>Avg Order</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#e67e22'>{{ $json.weekOverWeekChange }}%</div><div style='color:#7f8c8d;font-size:13px'>WoW Change</div></td></tr></table></body></html>"},"id":"dr4","name":"Send Email Report","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[900,200]},
    {"parameters":{"webhookUri":"YOUR_SLACK_WEBHOOK_URL","text":"={{ '📊 *Daily Report — ' + $json.date + '*n💰 Revenue: $' + $json.totalRevenue + 'n📦 Orders: ' + $json.orderCount + 'n📈 WoW: ' + $json.weekOverWeekChange + '%' }}"},"id":"dr5","name":"Post to Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[900,420]}
  ],
  "connections": {
    "Every Day at 7 AM":{"main":[[{"node":"Read Sales Data","type":"main","index":0}]]},
    "Read Sales Data":{"main":[[{"node":"Calculate KPIs","type":"main","index":0}]]},
    "Calculate KPIs":{"main":[[{"node":"Send Email Report","type":"main","index":0},{"node":"Post to Slack","type":"main","index":0}]]}
  },
  "settings":{"executionOrder":"v1"},
  "tags":[{"name":"reporting"}]
}

Setup (10 minutes)

  1. Import the JSON into n8n (New Workflow → Import from clipboard)
  2. Create your metrics sheet in Google Sheets with columns: Date, Revenue, Orders — or adapt the Code node to match your existing data
  3. Connect your Google account in both the Sheets and Gmail nodes (one OAuth connection covers both)
  4. Replace YOUR_SHEET_ID with your actual sheet ID — it’s in the URL: docs.google.com/spreadsheets/d/[SHEET_ID]/edit
  5. Replace [email protected] with your email address in the Gmail node
  6. (Optional) Set up a Slack Incoming Webhook and paste the URL in Node 5, or delete that node entirely
  7. Adjust the cron in Node 1 — 0 7 * * * fires at 7 AM UTC. Change 7 to any hour you prefer.
  8. Activate the workflow — it runs automatically from now on

First run: manually trigger Node 1 to verify it reads your sheet correctly before activating.

Customizations

Multiple data sources

Add more Sheets nodes before the Code node — one sheet for sales, one for support tickets, one for email open rates. In the Code node, access them by index: const salesRows = $input.all() (with a Merge node upstream to combine them).

Pull from Stripe or Shopify directly

Replace the Sheets node with an HTTP Request node hitting the Stripe API (/v1/charges?created[gte]=YESTERDAY_TIMESTAMP) or Shopify Orders API. Same Code node logic applies — just adjust the field names.

Weekly digest instead of daily

Change the cron to 0 8 * * 1 (every Monday at 8 AM). Update the Code node to sum the last 7 rows instead of just yesterday’s data.

Add traffic data

Add an HTTP Request node that calls the Google Analytics Data API for sessions, bounce rate, and top pages from the previous day. Merge it into the Code node alongside your sales data.

Color-coded status (green/red)

In the Code node, add: targetMet: parseFloat($json.totalRevenue) >= YOUR_DAILY_GOAL. In the email HTML, reference it to make the Revenue card green when you hit target and red when you miss.

Real impact

For a solo founder running an online store, this means waking up to a clean summary instead of opening 5 apps. You know within 10 seconds of looking at your phone whether yesterday was good.

For agency owners with multiple clients: add a loop over an array of sheet IDs. One workflow run, one report per client, all in your inbox by 7:05 AM.

The workflow runs on n8n — self-hosted, open source, no monthly per-workflow fees.

Get the full automation bundle

This workflow is part of the FlowKit 15-template n8n bundle — each template covers a different business use case: lead capture, invoice generation, AI customer support, social media automation, price monitoring, appointment reminders, and more.

Grab the full bundle at stripeai.gumroad.com — pre-tested, documented, ready to activate.

Built with n8n. Self-hostable, open source, no vendor lock-in.

Try to Hack My AI-Agent Workflow: GitHub Issues as a Control Surface

Two possible outcomes of the public challenge

I built a small public experiment.

You can open a GitHub Issue and ask an AI-agent workflow to create a static page on my website:

  1. If your request fits the rules, the workflow may turn it into a published page.
  2. If your request does not fit the rules, the agents should reject it and explain why.

And if you want to make the experiment more interesting, do not only submit a valid request. Try to bypass the boundary. Try to make the workflow publish something it should not publish. Then inspect the issue comments, labels, pull request, and final result.

This is the game.

But under that game, there is a more serious question:

Can external user intent enter a real product repository and become a bounded, reviewable product change without giving up product ownership?

That is what I am trying to explore.

The public playground is here: Demo Pages

The challenge

The surface rule is simple:

Create a GitHub Issue that requests a small static demo page.

The workflow should read the issue, decide whether the request belongs to the allowed area, run the required agent steps, create or update the page, validate the result, and publish it only if the process succeeds.

But this is not a public CMS.

It is not a hosting service.

It is not a website builder.

It is not a place where an external user should be able to change my homepage, global layout, styles, scripts, product pages, or site structure.

The allowed result is intentionally narrow: a bounded inert static page under the Demo Pages section.

So the challenge has two sides.

First, try the normal path:

Create a small demo page request and see whether the workflow can turn it into a published static artifact.

Then try the adversarial path:

Ask for something outside the boundary and see whether the workflow stops you.

For example, you can try to request a homepage change, inject instructions into the issue body, ask the agent to modify global styles, or try to smuggle forbidden behavior into the generated content.

Please keep the challenge inside the intended boundary. This is a workflow and prompt-boundary experiment, not an infrastructure attack. Do not attack the server, credentials, network, GitHub itself, availability, or anything outside the issue text and the public repository workflow.

The interesting question is not whether the server can be broken.

The interesting question is whether a chain of AI agents can defend a product boundary while still allowing useful delegated work.

Two possible outcomes of the public challenge

What you can inspect

The workflow is intentionally public enough to leave evidence.

A request should not disappear into a private chatbot.

A useful result should leave a visible trail:

  • the original issue;
  • the labels that describe state transitions;
  • comments written by agents;
  • a pull request when a change is produced;
  • validation evidence;
  • the published static page when the change is accepted.

The page itself is not the main artifact.

The main artifact is the trace.

A static page is only the visible end of the process. The more important part is the path from external request to controlled product change.

If the workflow accepts your request, you can inspect what happened.

If the workflow rejects your request, you can inspect why.

A successful rejection is also a useful result.

In ordinary automation, a rejection often feels like failure. In an AI-agent workflow, a correct rejection is evidence that the process has boundaries. It means the agent did not blindly satisfy the user request. It treated the issue as a signal, not as a command.

That distinction matters.

Why I built this

AI coding agents are getting better at producing code.

But code generation is not the hardest part of using agents in real products.

The harder problem is control.

  • Who allowed the agent to work?
  • What kind of request was considered safe?
  • Which files was the agent allowed to change?
  • What checks were required?
  • What evidence was recorded?
  • When should the agent stop and ask for a human decision?

If these questions are not answered, “autonomous agent development” easily becomes an uncontrolled prompt playground.

That may be interesting for demos, but it is not how I want agents to touch real products.

In a product environment, the owner must not lose control. Users can express intent. Agents can perform delegated work. But the product owner must define the paths where autonomous work is allowed, the boundaries where it must stop, and the evidence required before the result enters the product.

This experiment uses static demo pages because they are safe, visible, and easy to understand.

But the real subject is not static page generation.

The real subject is controlled product evolution.

A user opens an issue. The workflow checks whether the request belongs to an approved path. If it does, agents may process it. If it does not, the request should be rejected, blocked, or routed to human review.

That is the model I want to test in public.

The workflow behind the game

The current experiment uses GitHub Issues as the entry point.

A GitHub Issue is a familiar development artifact. It can describe a request, collect discussion, receive labels, trigger events, and preserve history. That makes it a practical control surface for agent-driven work.

The simplified route looks like this:

Workflow route from GitHub Issue to published demo page

The workflow does not treat the issue as a direct publishing request.

It treats the issue as a development signal.

That signal must pass through rules.

For the Demo Pages experiment, the allowed area is intentionally small. The agent may work only inside the documented demo-page boundary. It must not turn a reader request into arbitrary site changes.

This is where the workflow becomes more important than the generated page.

A simple page-generation demo could be built with one prompt and one script.

That is not the point here.

The point is to show a controlled route where request admission, agent execution, validation, pull request creation, merge, and publication are separate observable steps.

What protects the boundary

The boundary is not protected by asking the model to “please behave.”

That is not enough.

The workflow needs several kinds of control.

First, there is a documented scope.

The agent must know what kind of request is allowed, what area of the repository may be changed, and what must remain untouched.

Second, there is admission.

Before implementation starts, the request should be classified. A request that targets the homepage, global layout, scripts, styles, or product content should not be treated as a valid demo-page request.

Third, there is restricted execution.

Agents should run in a bounded environment and work through repository changes, not through direct production mutation.

Fourth, there is validation.

The result must be checked before it becomes a published artifact.

Fifth, there is evidence.

Labels, comments, pull requests, and logs matter because they make the process inspectable. Without evidence, autonomy is difficult to trust.

This is why I am inviting people not only to submit valid requests, but also to submit boundary-crossing requests.

A workflow that only succeeds on clean examples is not very interesting.

A workflow becomes more useful when it can say:

No, this request is outside the approved path, and here is why.

That answer is part of the product control model.

Why this may matter for product teams

This experiment looks small because the artifact is small.

A static page is not a large feature.

But the pattern is larger than the artifact.

Many teams already use GitHub Issues, pull requests, CI checks, reviews, and deployment pipelines. AI agents can enter this environment, but they need more than task prompts. They need process boundaries.

A product team does not only need an agent that can write code.

It needs a way to answer:

  • which requests can be delegated;
  • which requests require human review;
  • what evidence must be produced;
  • what validation is required;
  • how the owner can inspect and change the rules;
  • how the product avoids becoming controlled by random external prompts.

That is the commercial reason I care about this.

I am not trying to demonstrate that an AI agent can write a static page. That is already obvious.

I am trying to demonstrate that an AI-agent workflow can transform an external request into a controlled, reviewable, bounded product change.

For a real team, this pattern could apply to documentation fixes, small UI corrections, test additions, generated examples, controlled content updates, internal tools, or other well-bounded change paths.

The important part is not the specific artifact.

The important part is the governance loop:

intent -> boundary check -> delegated work -> evidence -> review -> product change

What powers this experiment

The public workflow is powered by my GitHub Flows work.

The host application is here: github-flows-app

It is a ready-to-run host application around @teqfw/github-flows. It starts a Node.js process, loads local runtime configuration, exposes the GitHub webhook ingress, provides a workspace for configuration and logs, and allows the workflow runtime to react to GitHub events.

The host application itself is not the whole idea.

The broader idea is that repository events can drive agent execution through an explicit process. An issue is opened. A label is added. A pull request is created. A validation result appears. A merge happens. These events can move work through a controlled chain instead of relying on one long manual chat session.

I described the conceptual layer here: Turning User Intent Into Controlled Product Evolution

That article explains the model behind this experiment: users express intent, the product owner defines approved paths, agents act as delegated intermediaries, and the product changes only through bounded, observable, reviewable steps.

Try it

Start here: Demo Pages

Try a simple valid request first.

Then try a request that should be rejected.

Look at the labels.

Read the agent comments.

Follow the pull request if one appears.

Open the published page if the request reaches publication.

If the workflow rejects you, that may be the correct result.

If the workflow publishes something it should not publish, I want to know.

And if you are working with a real product team and thinking about how AI agents could safely participate in GitHub-based development, I am interested in practical cases.

The question is no longer only:

Can an AI agent write code?

The more useful question is:

Can we give agents limited authority inside a process that product owners can control?

If that is the kind of problem you are facing, you can contact me.

# I built a type-safe SQL library for Bun — no ORM, no codegen, just SQL (using Claude Code)

I’ve been using Bun for a while and kept running into the same problem: every SQL library either requires Node.js internals, leans heavily on an ORM abstraction I don’t want, or generates types from a schema file at build time.

So I built squn — a lightweight, type-safe SQL query library that works natively with Bun’s built-in database clients.

The core idea

Every query goes through a tagged template literal called sql. Interpolated values always become bound parameters — they are never concatenated into the SQL string. SQL injection is structurally impossible by design.

import { createDb, PostgresAdapter, sql } from "@phonemyatt/squn";

const db = createDb(new PostgresAdapter({
  url: "postgresql://user:password@localhost:5432/mydb",
}));

interface User {
  id: number;
  name: string;
  age: number | null;
}

// Values become $1, $2 parameters — never string-concatenated
const users = await db.query<User>(sql`SELECT * FROM users WHERE age > ${18}`);

No schema file. No code generation step. No build-time magic. You write SQL, get back typed results.

Four databases, one API

squn supports all four databases you’re likely to use with Bun:

Database Driver
SQLite bun:sqlite (built-in)
PostgreSQL Bun’s native Postgres
MySQL Bun’s native MySQL
MSSQL mssql npm package

The same query code works across all four — only the adapter construction changes.

// Switch databases by swapping the adapter
const db = createDb(new SqliteAdapter({ filename: ":memory:" }));
const db = createDb(new PostgresAdapter({ url: process.env.PG_URL }));
const db = createDb(new MysqlAdapter({ url: process.env.MYSQL_URL }));
const db = createDb(new MssqlAdapter({ host: "localhost", ... }));

Query methods that match what you actually need

// All rows
const users = await db.query<User>(sql`SELECT * FROM users`);

// First row or null — no throw
const user = await db.queryFirst<User>(sql`SELECT * FROM users WHERE id = ${1}`);

// Exactly one row — throws if 0 or 2+ rows returned
const user = await db.querySingle<User>(sql`SELECT * FROM users WHERE id = ${1}`);

// Scalar — first column of first row
const count = await db.queryScalar<number>(sql`SELECT COUNT(*) FROM users`);

Composable SQL fragments

Fragments compose. Nested fragments merge inline and placeholders are renumbered automatically.

const minAge = 18;
const activeOnly = true;

const conditions = [
  sqlIf(minAge !== undefined, sql`age >= ${minAge}`),
  sqlIf(activeOnly, sql`active = ${true}`),
];

const where = sqlJoin(conditions, " AND ");
const q = sql`SELECT * FROM users WHERE ${where} ORDER BY name`;
// → SELECT * FROM users WHERE age >= $1 AND active = $2 ORDER BY name
// params → [18, true]

No string concatenation. No injection risk. Full composability.

Transactions that don’t leak

atomically wraps your callback in BEGIN/COMMIT and rolls back automatically on error:

await db.atomically(async (q) => {
  await q.execute(sql`UPDATE accounts SET balance = balance - ${100} WHERE id = ${from}`);
  await q.execute(sql`UPDATE accounts SET balance = balance + ${100} WHERE id = ${to}`);
  // if either throws, both updates are rolled back
});

Transaction also implements Symbol.asyncDispose — so await using gives you guaranteed cleanup:

await using tx = new Transaction(await adapter.beginTransaction());
await tx.execute(sql`UPDATE users SET active = ${false} WHERE id = ${42}`);
await tx.commit();
// if commit throws or you return early, rollback happens automatically

Batch inserts with a single prepared statement

await db.executeBatch(
  sql`INSERT INTO users (name, age) VALUES (@name, @age)`,
  [
    { name: "Alice", age: 30 },
    { name: "Bob",   age: 25 },
    { name: "Carol", age: 35 },
  ],
);

One prepared statement, all rows bound in a loop. Much faster than individual inserts.

Type inference from table definitions

Define your table schema once, get insert/select/update types inferred automatically:

import { col, defineTable, InferSelect, InferInsert } from "@phonemyatt/squn";

const Users = defineTable({
  id:   col("integer").primaryKey().notNull(),
  name: col("text").notNull(),
  age:  col("integer").nullable(),
});

type UserRow    = InferSelect<typeof Users>;  // { id: number; name: string; age: number | null }
type UserInsert = InferInsert<typeof Users>;  // { name: string; age?: number | null }

Multi-connection and read replicas

const db = createConnections({
  connections: {
    primary: new PostgresAdapter({ url: process.env.PRIMARY }),
    replica: new PostgresAdapter({ url: process.env.REPLICA }),
  },
  default: "primary",
});

// Route reads to replica
const users = await db.query<User>(sql`SELECT * FROM users`, { connection: "replica" });

// Scoped helper — no connection option needed per call
const replica = db.use("replica");

// Typed concurrent queries
const [users, roles] = await db.concurrent(
  db.query<User>(sql`SELECT * FROM users`),
  db.query<Role>(sql`SELECT * FROM roles`),
);

Try it

bun add @phonemyatt/squn

Feedback welcome — especially from anyone using it with MySQL or MSSQL in production.

Built with TypeScript 5.9 strict mode, zero any, and tested against real databases in Docker.

DynamoDB Contributor Insights Pricing: What You’re Actually Paying For

/$0.50 per rule per month. $0.03 per million events. Enabling Contributor Insights on a single DynamoDB table auto-creates four rules so your floor is $2.00/month before a single event is counted.

At 500 million events/month, that’s $17.00/month. At 5 billion, it’s $152.00/month.

Whether that’s justified depends on your traffic volume, whether you have hot key patterns, and what throttling is already costing you in wasted provisioned throughput.

What Is DynamoDB Contributor Insights?

Contributor Insights for DynamoDB is a CloudWatch monitoring feature that identifies the top partition keys driving read and write traffic on a table without any code-level instrumentation or custom application logging.

It analyzes request traffic in near real-time and publishes results as CloudWatch graphs. For tables with partition key skew, it answers the specific question a FinOps engineer needs: “Which partition key is causing throttling?”

The Two Pricing Components

Contributor Insights billing has exactly two line items on your CloudWatch invoice:

  • Rule fee: $0.50 per rule per month, flat regardless of traffic
  • Event fee: $0.03 per 1 million DynamoDB read/write events matched

How many rules does one table create?

Enabling on a single table auto-creates four rules:

  • Most-accessed partition keys on the base table
  • Most-accessed partition keys causing errors on the base table
  • Most-accessed partition keys on each GSI
  • Most-accessed partition keys causing errors on each GSI

Rule count formula: 2 + (2 × number of GSIs)

A table with no GSIs = $2.00/month in rule fees. A table with 3 GSIs = $4.00/month in rule fees, before any events.

What counts as an “event”?

Every read and write operation: GetItem, PutItem, UpdateItem, DeleteItem, BatchGetItem, BatchWriteItem (each item individually), Query and Scan (each item returned or examined), and transactional reads/writes. GSI operations are counted separately when rules are active on both.

Worked Cost Examples

10M events/month: $2.00 rule fee + $0.30 event fee = $2.30/month

500M events/month: $2.00 rule fee + $15.00 event fee = $17.00/month

2B events/month: $2.00 rule fee + $60.00 event fee = $62.00/month

2B events/month + 3 GSIs: $4.00 rule fee + $60.00 event fee = $64.00/month

For most workloads, the event fee is the variable that matters. The rule fee is fixed overhead.

Throttle-Only Mode

As of August 2025, AWS added a throttle-only mode. Rules only activate for events that result in throttling requests that exceed provisioned capacity or on-demand burst limits.

You still pay the $0.50/rule/month rule fee. But the event fee drops dramatically. On a well-provisioned table with a 0.1% throttle rate processing 2 billion events/month, you’d match roughly 2 million throttled events, an event cost of $0.06/month instead of $60.00/month.

Use throttle-only mode when:

  • You only need to diagnose which keys are causing throttling, not which are most popular
  • Your table is currently healthy and you want passive monitoring with near-zero cost
  • You have a high-volume table (1B+ events/month) where full mode runs $30+/month

Use full mode when:

  • You’re actively investigating a hot key pattern
  • You want continuous visibility into traffic distribution
  • You’re evaluating whether to restructure your partition key schema

If you’re deciding between 1-year and 3-year AWS commitment terms before locking in Reserved Capacity, the tradeoffs are covered in detail here How to Choose Between 1-Year and 3-Year AWS Commitments

The Real Cost Question: What Does Hot Key Blindness Cost?

Contributor Insights costs $2 to $62/month. The cost of not having it shows up in your provisioned capacity bill every month.

DynamoDB provisioned capacity is distributed across partitions. If your table is configured for 10,000 RCUs/second and 90% of reads go to three partition keys, the capacity on your other partitions sits idle while those three keys throttle.

Common patterns where this happens:

  • Low-cardinality partition keys (e.g., user_type, region, status)
  • Popular cached items that expire simultaneously, causing a read surge on one key
  • Time-based queries where all traffic clusters around “today” or “current”

A concrete scenario: a table provisioned at 5,000 WCUs/second costs approximately $2,847/month in write capacity (verify current rates at aws.amazon.com/dynamodb/pricing). If hot key throttling means 30% of that capacity is sitting idle on underutilized partitions, you’re paying ~$854/month for throughput you can’t use.

Contributor Insights at $17/month to identify which keys to redesign around: that’s a 50:1 return in the month you act on it.

*Hot Keys and Reserved Capacity Commitments
*

This is where hot key detection connects directly to cost optimization strategy.

DynamoDB Reserved Capacity delivers 30–40% savings on provisioned throughput. But those savings assume your capacity is being utilized effectively. A table with undetected hot key patterns has two compounding problems:

  • It’s over-provisioned because teams add capacity to compensate for throttling instead of fixing the partition key design
  • When you commit to Reserved Capacity on an over-provisioned table, you lock in the waste

The right sequence: identify hot keys via Contributor Insights → fix the partition key schema → right-size provisioned capacity → apply Reserved Capacity on that right-sized number. The result is a smaller commitment with higher utilization and 30–40% off the correct baseline.

Contributor Insights vs Standard CloudWatch Metrics

CloudWatch already provides DynamoDB metrics by default, at no charge. The key difference is specificity.

Standard CloudWatch metrics tell you “you have 50,000 throttled requests this hour.” Contributor Insights tells you “partition key user#12345 accounts for 48,000 of those throttled requests.”

For active troubleshooting or capacity planning, that distinction is the difference between guessing and acting.

When to Enable It

Enable Contributor Insights when:

  • Your table experiences recurring throttling that standard metrics can’t explain
  • You’re planning a table migration or partition key schema redesign
  • Your table processes 100M+ events/month and you have no visibility into traffic distribution
  • You’re considering a Reserved Capacity commitment and want to validate utilization first

Use throttle-only mode when:

  • You want passive monitoring with near-zero event costs
  • Your table is healthy but you want early warning if hot keys develop

Skip it entirely when:

  • You have very high cardinality partition keys (e.g., UUID-based) with no known skew
  • You’re on on-demand capacity mode with confirmed even traffic distribution
  • The table is non-production with no Reserved Capacity commitment

How to Enable It

Prerequisites: IAM permissions for cloudwatch:EnableInsightRules and dynamodb:DescribeTable. Understand the event cost for your traffic volume before enabling full mode.

  • Open the DynamoDB console and select your target table
  • Go to the Monitor tab
  • Click Manage Contributor Insights; select “All Requests” (full mode) or “Throttled Requests Only” (throttle-only mode)
  • Navigate to CloudWatch > Contributor Insights and confirm the rules are in “Enabled” state
  • Allow 15–30 minutes for data to appear; CloudWatch will show the top 10 partition keys by request volume

If rules show an “Error” state, the most common cause is insufficient IAM permissions. Verify the execution role includes cloudwatch:EnableInsightRules, cloudwatch:PutInsightRule, and dynamodb:DescribeTable.

How This Fits With Reserved Capacity

Identifying hot keys is a prerequisite to maximizing Reserved Capacity ROI. A table with hot key throttling shows a deceptive utilization signal: total consumed capacity may look high, but actual table-level efficiency is low because hot partitions throttle while cold partitions sit idle.

Usage.ai’s Flex Reserved Instances handle DynamoDB Reserved Capacity commitments with no upfront payment, no multi-year lock-in, and a cashback guarantee if utilization falls short see how it works at usage.ai

Usage.ai refreshes DynamoDB recommendations every 24 hours, vs. the 72+ hour refresh cycle on AWS native tools. At DynamoDB pricing, that 3-day gap in detecting newly-emerged hot key patterns can represent meaningful daily waste before any action is taken.

Have you run into hot key issues on DynamoDB? Did you catch it via Contributor Insights or some other way and what did the fix end up looking like?

Explore the full operational breakdown here → DynamoDB Contributor Insights Pricing: Monitoring Hot Keys at a Cost

Cloudflare Completes Its Agent Infrastructure Stack with Browser Run Rebuild and Six-Layer Platform

Cloudflare rebuilt Browser Run on its own Containers platform, delivering 4x higher concurrency and 50% faster response times. The upgrade completes a six-layer agent infrastructure stack: compute (Dynamic Workers + Sandboxes), orchestration (Dynamic Workflows), memory (Agent Memory), browsing (Browser Run), and commerce (Stripe Projects).

By Steef-Jan Wiggers

Oracle Linux 7 to 8 Upgrade Using Leapp: Architecture, Inhibitors, and Enterprise Troubleshooting

Introduction

Enterprise Linux operating systems require periodic upgrades to maintain security, supportability, compliance, and operational stability.

As organizations modernize infrastructure platforms, migrating from Oracle Linux 7 to Oracle Linux 8 becomes important because Oracle Linux 8 introduces:

✔ Modern package management
✔ Improved security
✔ Better kernel support
✔ Enhanced automation compatibility
✔ AppStream modular repositories
✔ Long-term enterprise support

However, major Linux upgrades are not simple package updates.

They involve:

✔ Repository transitions
✔ Package dependency changes
✔ Kernel migration
✔ Bootloader modifications
✔ Service compatibility validation
✔ Third-party package handling

Oracle Linux provides the Leapp upgrade utility to automate and orchestrate Oracle Linux 7 to Oracle Linux 8 migrations safely.

In this blog, we will perform a complete deep dive into:

✔ Configuring Leapp repositories
✔ Installing Leapp utility
✔ Preupgrade analysis
✔ Understanding inhibitors
✔ Answer file handling
✔ Repository migration
✔ Upgrade execution
✔ Upgrade boot workflow
✔ Enterprise troubleshooting
✔ Real-world operational challenges

Understanding Leapp

Leapp is an in-place upgrade utility used to migrate Oracle Linux 7 systems to Oracle Linux 8.

Why Major OS Upgrades Are Complex

Upgrading from Oracle Linux 7 to Oracle Linux 8 involves platform-level architectural changes.

These include:

✔ Kernel transitions
✔ Package replacement
✔ Repository mapping
✔ Driver compatibility
✔ Service migration
✔ Security policy updates
✔ Boot environment changes

Note : Operating system upgrades are not only package upgrades — they are full platform transitions.

Oracle Linux Upgrade Architecture

The Leapp upgrade process follows multiple operational stages.

Upgrade Workflow

Oracle Linux 7
│
▼
Repository Validation
│
▼
Leapp Installation
│
▼
Preupgrade Analysis
│
▼
Inhibitor Detection
│
▼
Answer File Validation
│
▼
Upgrade Initramfs Creation
│
▼
System Reboot
│
▼
Upgrade Environment Boot
│
▼
Package Migration
│
▼
Oracle Linux 8

Step 1: Verify Current Oracle Linux Version

Before starting the migration, validate the current OS version.

Command:

cat /etc/os-release

Example output:

NAME=”Oracle Linux Server”
VERSION=”7.x”

Step 2: Verify Current Repositories

Repository consistency is critical before performing upgrades.

Check repositories:

yum repolist

Why Repository Validation Matters

Broken or duplicate repositories may cause:

✔ Dependency failures
✔ Package mapping errors
✔ Upgrade inhibitors
✔ Incomplete migrations
✔ Boot failures

Add Leapp Repository Configuration

Navigate to repository directory:

cd /etc/yum.repos.d/

Create or validate Oracle Linux repositories.

Example:

[ol7_leapp]
name=Oracle Linux 7 Leapp Repository
baseurl=https://yum.oracle.com/repo/OracleLinux/OL7/leapp/x86_64/
enabled=1
gpgcheck=1

Refresh Repository Metadata
yum clean all
yum makecache

Step 4: Install Leapp Utility

Install required Leapp packages.

Command:

yum install -y leapp-upgrade leapp-data-oraclelinux

What Gets Installed?

Leapp installs:

✔ Upgrade actors
✔ Dependency analysis modules
✔ Migration logic
✔ Repository mapping data
✔ Upgrade workflows

Verify Leapp Installation

rpm -qa | grep leapp

Step 5: Understanding Leapp Preupgrade

Before performing the actual upgrade, Leapp performs extensive system analysis.

Command:
leapp preupgrade

What Happens During Preupgrade?

Leapp analyzes:

✔ Installed packages
✔ Drivers
✔ Repository configuration
✔ Bootloader state
✔ Kernel compatibility
✔ Unsupported packages
✔ Dependency conflicts
✔ Security policies

Operational Insight
The preupgrade phase prevents unsafe migrations before system modification begins.

Understanding Inhibitors

One of the most important Leapp concepts is inhibitors.

What Are Inhibitors?

Inhibitors are conditions that stop the upgrade from continuing safely.

If inhibitors exist, Leapp blocks the upgrade process.

Why Inhibitors Exist

Inhibitors protect systems from unsafe migration scenarios.

Examples:

✔ Unsupported repositories
✔ Duplicate repository entries
✔ Deprecated packages
✔ Missing answer files
✔ Third-party RPM conflicts
✔ Unsupported drivers
✔ Incorrect boot configuration

Real Repository Inhibitor Example

Example error:

Repository ol8_baseos_latest is listed more than once in the configuration.

Why This Happens

Possible causes:

✔ Duplicate .repo files
✔ Custom repositories
✔ Third-party repositories
✔ Incorrect migration preparation

Fixing Duplicate Repositories

Check repository directory:

ls -l /etc/yum.repos.d/

Review duplicate repository definitions:

grep -r “ol8_baseos_latest” /etc/yum.repos.d/

Remove duplicate entries carefully.

Understanding Leapp Answer Files

This is one of the most important upgrade concepts.

What Are Answer Files?

During upgrades, Leapp may require administrator confirmation for specific migration decisions.

Leapp stores these prompts inside answer files.

Location:

/var/log/leapp/answerfile

Why Answer Files Matter

Leapp blocks upgrades until required questions are answered.

Example:

Missing required answers in the answer file.

View Required Answers

Command:

leapp answer –section remove_pam_pkcs11_module_check.confirm=True

What Does This Do?

This command confirms specific upgrade actions required by Leapp.

Operational Insight

Answer files help administrators explicitly approve risky or environment-specific migration decisions.

Understanding Repository Migration

Oracle Linux 7 and Oracle Linux 8 use different repository structures.

Oracle Linux 7 Repositories

✔ ol7_latest
✔ ol7_UEKR6
✔ Optional repositories

Oracle Linux 8 Repositories

✔ BaseOS
✔ AppStream
✔ UEK repositories

Repository Mapping Workflow

OL7 Repositories
│
Repository Mapping
│
▼
OL8 BaseOS + AppStream

Understanding AppStream Repositories

Oracle Linux 8 introduces AppStream modular repositories.

Unlike Oracle Linux 7:

Packages are grouped into modules and streams.

Examples:

✔ Python streams
✔ NodeJS streams
✔ Database modules

This increases flexibility but also migration complexity.

Third-Party Repository Challenges

Enterprise systems commonly use third-party repositories.

Examples:

✔ EPEL
✔ Monitoring agents
✔ Security tools
✔ Vendor repositories
✔ Backup software

Example EPEL Problem

Example error:

No package epel-release available
Why This Happens

Possible causes:

✔ Repository incompatibility
✔ Incorrect release version
✔ Unsupported packages
✔ Missing metadata

Step 6: Execute Upgrade

Once inhibitors are resolved, begin the upgrade.

Command: leapp upgrade

What Happens Internally?

Leapp performs:

✔ Upgrade initramfs creation
✔ Bootloader modification
✔ Package migration
✔ Repository transition
✔ Kernel migration
✔ Service migration
Understanding Upgrade Initramfs

This is one of the most advanced upgrade concepts.

What is Upgrade Initramfs?

Leapp temporarily boots into a dedicated upgrade initramfs environment outside the running Oracle Linux 7 userspace.

This isolated environment safely performs package replacement operations.

Upgrade Boot Workflow

Normal OL7 Boot
│
▼
Upgrade Initramfs
│
▼
Package Migration
│
▼
OL8 Boot

Why Upgrade Initramfs Is Required

Leapp performs the upgrade outside the running Oracle Linux 7 userspace to avoid active package conflicts and dependency corruption during migration.

The temporary upgrade initramfs environment provides an isolated userspace where package replacement, repository switching, kernel migration, and dependency updates can occur safely without interfering with the currently running operating system.

This isolation significantly reduces the risk of package inconsistency and upgrade instability during major platform transitions.

Step 7: Reboot the System

After upgrade preparation:

reboot

The system boots into the temporary upgrade environment.

Migration occurs automatically.

Step 8: Verify Oracle Linux 8 Upgrade

After reboot completes:

Verify OS Version
cat /etc/os-release

Common Real-World Upgrade Problems

Many upgrade failures occur because of environment inconsistencies rather than Leapp itself.

Common Operational Problems

✔ Repository duplication
✔ Dependency conflicts
✔ Unsupported packages
✔ Missing drivers
✔ Bootloader issues
✔ EPEL incompatibility
✔ Service startup failures
✔ Network configuration mismatches
✔ SELinux conflicts

Upgrade Failure Recovery

If the upgrade process fails during migration, administrators should analyze Leapp reports, validate repositories, review inhibitors, and restore systems using backups or boot volume snapshots when necessary.

/var/log/leapp/leapp-report.txt

/var/log/leapp/leapp-upgrade.log

/var/log/leapp/leapp-preupgrade.log

Leapp logs provide detailed visibility into dependency analysis, migration stages, repository transitions, and package failures.

Rollback and Recovery Planning

Enterprise upgrades should always include rollback preparation.

Before upgrades:

✔ Create backups
✔ Snapshot boot volumes
✔ Validate rollback workflows
✔ Test upgrades in staging
✔ Document recovery procedures

Why Rollback Planning Matters

If upgrades fail unexpectedly:

Rollback capability reduces downtime and operational risk.
Enterprise Upgrade Best Practices

✔ Validate backups before upgrade
✔ Remove unsupported repositories
✔ Review Leapp reports carefully
✔ Resolve inhibitors completely
✔ Test upgrades in staging first
✔ Validate applications after migration
✔ Monitor services post-upgrade
✔ Maintain rollback procedures

Conclusion

Leapp provides a powerful and automated framework for migrating Oracle Linux 7 systems to Oracle Linux 8 through dependency analysis, repository validation, package migration, and upgrade orchestration.

Although the migration process is heavily automated, successful enterprise upgrades still require careful planning, repository consistency, inhibitor analysis, answer file validation, operational testing, and rollback preparation to ensure production stability.

Modern Linux upgrades are no longer simple package updates — they are enterprise platform modernization workflows requiring operational engineering discipline.

Responsive Web Design

Beginning:

Responsive web design is a web design or CSS style that makes a website look good on any screen size.

Note: The most popular responsive design technique is “Mobile First Design”.

To make the page optimized for mobile devices, first use this meta tag in the “head”
Сode:
<meta name="viewport" content="width=device-width, initial-scale=1" />

CSS Responsive Design can be achieved by using:

  • CSS units as percentages “%” (100%), “1fr” (for grid). “vw”.
  • “media query”.
  • CSS property: “max-width”, “min-width”.
  • Maybe something else.

A few of my own examples of what I use:

  1. If the width of the screen is 500 pixels or less, we do something, or if it is more than 500 pixels, we do something else.

@media(max-width: 500px){
p {
color: red;
}
}

@media(min-width: 500px){
p {
color: blue;
}
}

  1. This CSS code prevents images from overflowing and sets the automatic size for the picture:

img {
max-width:100%;
height: auto;
display: inline-block;
}

  1. My responsive content or page “wrapper” class. This wrapper in the example makes the content no wider than 560px and a width of 100% makes it adaptive to any screen:

.wrapper {
width: 100%;
max-width: 560px;
margin: 0 auto;
}

  1. Hide 1 and show 2 something depending on the screen size or dynamic style change. Hide the desktop version and show the mobile version of something instead. (A drop down menu or something else):

@media(width >= 500px){
p { color: red; }
}

@media(width <= 500px){
p { color: green; }
}

or

@media(width >= 500px){
.mobileVersion { display: none; }
.desktopVersion { display: block; color: red }
}

@media(width <= 500px){
.mobileVersion { display: block; color: green; }
.desktopVersion { display: none; }
}

  1. Personal experience and advice or technique:

I write CSS styles simultaneously for any screen, my method is the method of overriding the style above with the style below (hierarchy in CSS), for example, there is a regular style for large screens:

.testClass { color: red; }

Then under it I add the same CSS class and properties from the style above with values for other screens using CSS Media Queries:

.testClass { color: red; }

@media (max-width: 500px) {
.testClass { color: orange; }
}

@media (max-width: 300px) {
.testClass { color: blue; }
}

@media (max-width: 100px) {
.testClass { color: yellow; }
}

so I override the style above and create a new style with overriding method for screens that are not larger than 500 pixels, then for 200px with overwriting , then for 100px etc

  1. Note: Testing and debugging:
  • Resize the browser window to see the style changes.
  • Launch the developer tool in the browser through the menu and “Inspect” page and change the size of the window there.
  • In developer tools, you can choose mobile phone simulation: “Responsive Design Mode” (phone icon).
  • Of course, a test on a physical device.

Source and Learning:

Doc: