Walking through a 5-domain Microsoft 365 audit in 30 seconds

If you administer a small Microsoft 365 tenant, here’s the question that probably stopped you somewhere between “I should check our security posture” and actually doing it:

Which tool?

Microsoft Secure Score gives recommendations but no remediation artifacts. CISA ScubaGear is excellent but federal-grade, overkill for a 20-person mid-market shop. M365DSC is configuration-as-code, which is great when you’re a DSC shop and terrible when you’re not. CIPP is purpose-built for MSPs managing many tenants, and solo defenders don’t need that fan-out.

There was a gap: an opinionated audit-plus-remediation toolkit for a solo defender running M365 + Cloudflare in a small org. I built one. Tagged 1.0 today.

github.com/ibondarenko1/m365-security-operations

What it actually does

One PowerShell command sweeps five domains, produces a single markdown report ranking findings P1/P2/P3, and links every gap to a ready-to-deploy remediation artifact:

Domain Audit Remediation
Sentinel detection engineering Workspace state, daily quota, retention, Sentinel onboarding, analytics rules, Fusion, Activity Log diagnostic, data connectors, workbooks, hunting queries, automation playbooks, watchlists, UEBA, threat intelligence 5 MITRE-mapped ARM templates + 10 KQL hunting drills
Defender for Office 365 Anti-phish impersonation, anti-spam, anti-malware, Safe Attachments, Safe Links, Tenant Allow/Block List, DKIM, ZAP, outbound thresholds, transport rules, Attack Simulation Training Exchange Online PowerShell remediation scripts
DNS + email authentication MX, SPF, DKIM, DMARC, MTA-STS, TLS-RPT, BIMI, NS, Autodiscover, CAA, DNSSEC, SPF lookup count, DMARC sub-policy Cloudflare Worker + DNS deployment script for MTA-STS + TLS-RPT
Identity hardening Conditional Access policies, authorization policy, directory roles, sign-in logs, authentication methods, app consent, service principal credentials, named locations, cross-tenant access, sign-in risk, SSPR 6 baseline Conditional Access policy JSONs ready for Graph PUT
Defender for Cloud Per-plan pricing tier, Secure Score, recommendations by severity, AI plane, continuous export to Sentinel Plan-tier upgrade methodology + walkthrough

Every finding is tagged with framework controls: NIST CSF 2.0, NIST SP 800-53, NIST SP 800-63B, ISO 27001:2022, MITRE ATT&CK, Microsoft Cloud Security Benchmark, RFC references.

Try it in 30 seconds

git clone https://github.com/ibondarenko1/m365-security-operations
cd m365-security-operations
./examples/run-mock.ps1

That runs the full audit against bundled sanitized fixtures and produces a complete sample report (58 findings across 5 domains). No Azure access required. Open reports/<latest-timestamp>/report.md to see exactly what the tool produces.

When you’re ready to run against your tenant:

az login --tenant <your-tenant-id>
./run-audit.ps1 -TenantId <id> -SubscriptionId <id> -Domain <yourdomain> -WorkspaceName <ws> -ResourceGroup <rg>

Why opinionated scope matters

This toolkit explicitly does NOT cover:

  • Multi-tenant MSP management (use CIPP)
  • Federal compliance overlays (use ScubaGear)
  • On-premises Active Directory (use Defender for Identity)
  • Endpoint detection at device level (use Defender for Endpoint native)
  • Data Loss Prevention (use Microsoft Purview)

The toolkit is opinionated for small-org cloud-only M365 + Cloudflare. Concentration enables depth: each domain has 15-25 checks, not the surface-level 5 a broader-scope tool can maintain.

Architecture you can actually contribute to

  • Schema-first: every audit script emits findings conforming to SCHEMA.md, enforced by lib/Finding.psm1
  • Mock mode: lib/MockClient.psm1 provides drop-in mocks for Graph + ARM + DNS + EXO. Contributors iterate on audit logic without burning real-tenant quota
  • 114 Pester tests in CI on Windows + Linux + Mac
  • 6 Architecture Decision Records document the design rationale
  • 5 walkthroughs cover end-to-end deployment of the remediation artifacts

What’s next

v1.0 is the public-release baseline. Roadmap continues with v1.1-v1.5 expanding per-domain checks, adding documentation_url to every finding (currently P1/P2 only), and surfacing community-contributed checks.

If you administer M365 in a small org, give it a try and open issues for what you’d like to see next.

github.com/ibondarenko1/m365-security-operations

MIT licensed. Methodology, schema, fixtures, walkthroughs, ADRs, Pester tests, and CI matrix all in the repo.

WebMCP and the Browser AI Layer: What Next.js Devs Need to Know

WebMCP and the Browser AI Layer: What Next.js Devs Need to Know

The dev.to MCP discourse this week is predictably binary — either “WebMCP will destroy how browsers work” or “it’s just another spec that won’t ship.” Both framings skip the question that actually matters for practitioners: when a browser speaks MCP natively, what specifically changes in a Next.js app, and what stays the same?

I’ve been watching the WebMCP proposal since it surfaced and I want to give you a grounded read — not hype, not dismissal.

What WebMCP actually is (and isn’t)

MCP (Model Context Protocol) is a protocol for letting AI agents communicate with tools and data sources in a structured way. You’ve likely used it via the Node.js SDK: you define server capabilities, expose them, and an LLM host calls them.

WebMCP is the proposed browser-native version of that same protocol — a JavaScript API that lets a webpage (or a web application) act as an MCP client directly inside the browser, without proxying through a backend server. The browser itself becomes an MCP-aware runtime.

What it is not: a replacement for your Next.js API routes. It is not a way to skip your backend. And it is not — at the time I’m writing this — a shipped standard. It’s a proposal with real momentum (Google showed a demo at I/O, the Chromium bug tracker has an entry, and multiple browser teams are watching), but momentum is not a ship date.

The distinction matters because several posts this week conflate “WebMCP exists as a spec” with “your existing Next.js app is already obsolete.” It’s not.

What actually changes for your Next.js routes

The meaningful shift is about who initiates the MCP conversation.

Today, if your Next.js app integrates with an AI agent, the flow looks like this: browser → Next.js API route → MCP server → LLM. Your Next.js route is the trust boundary. It holds the credentials, it validates the request, it shapes the response.

With WebMCP, a browser-native MCP client can connect directly to an MCP server — your server — without going through the Next.js route at all. The flow becomes: browser (WebMCP client) → MCP server.

Here is a minimal illustration of what that means for a capability you might expose today:

// Today: your Next.js API route wraps the MCP capability
// app/api/search/route.ts
export async function POST(req: Request) {
  const { query } = await req.json();
  // validate session, rate-limit, sanitise
  const result = await mcpClient.callTool('search', { query });
  return Response.json(result);
}

// With WebMCP: the browser calls your MCP server directly.
// Your route no longer sits between the browser and the capability.
// Anything that was implicitly safe because it was server-side
// now needs to be explicitly safe at the MCP server boundary.

The security model has to move from the HTTP route layer to the MCP server itself. If your MCP server today relies on “only my backend calls this,” that assumption breaks with WebMCP.

What to watch before you ship anything WebMCP-adjacent

Three things I’m tracking — and you should too.

1. The trust boundary shift is not free. Every tool you expose through an MCP server needs to be safe to call from an untrusted browser context, not just from your backend. That means per-user auth at the MCP layer, rate limiting at the MCP layer, and input validation at the MCP layer — none of which your API route was previously responsible for. I wrote about the prompt injection surface area that MCP servers already carry in a separate post on prompt injection risks I documented — the WebMCP expansion makes that surface much wider.

2. The spec is still moving. OAuth-style per-user sessions for WebMCP are proposed but not finalised. The browser API shape changed at least twice between I/O and the current draft. Building on top of a draft spec is fine for exploration but not for production. I’d build the MCP server side of your stack now — that’s stable — and treat the browser client as a layer you’ll wire up later.

3. SEO and AEO implications are real but indirect. If AI browsers start rendering MCP-powered responses inside the browser chrome (not your page), what gets indexed changes. The page content that currently signals authority to crawlers may not be what the AI browser layer actually presents to the user. This is the piece of the puzzle I’m watching most closely — it’s still early, but it’s the same pattern I’ve been mapping in my post on agentic AI production architecture where the rendering and the indexing layer start to diverge.

What to do right now (practically)

You don’t need to refactor anything today. But you should do two things:

Audit your MCP server’s trust assumptions. Walk every tool your MCP server exposes and ask: “If an untrusted browser client called this directly, what’s the worst that happens?” If the answer involves data exfiltration, privilege escalation, or uncapped resource use — that’s your prep list for when WebMCP ships.

Track the spec, not the takes. The Chromium intent to prototype and the WebMCP GitHub repo are better signal than blog posts (including this one). Subscribe to the MCP spec changelog. When the browser teams move from “intent to prototype” to “origin trial,” that’s when you actually need to ship something.

I built an agentic workspace that hit several of these boundary problems in production — the patterns I used there are the same ones WebMCP will put pressure on at the browser layer.

If you want a deeper read on building production-grade AI agent systems, I cover the architecture decisions in more detail on mudassirkhan.me.

If you want this kind of thing wired up on your stack end to end — MCP servers, Next.js integration, trust boundary design — that is exactly the kind of work I take on.

Drop a comment if you’re already running an MCP server behind a Next.js route — curious what your auth layer looks like and whether you’ve thought about the WebMCP scenario yet.

The Make.com Modules I Actually Use Every Week (And The Ones I Killed)

The problem with my Make.com setup

I use Make.com a lot. Client work, my own projects, experiments, tracking weird biohacking data, even baseball stuff. It started simple. Then, like every tool I enjoy, it turned into a zoo.

Too many scenarios. Too many modules. Too many runs where I had to ask: “Why did this even fire?”

A few months ago I did a hard audit of all my Make scenarios. Not a theoretical one. I looked at what ran weekly, what broke, what I ignored, and what quietly ate time and operations without giving anything back.

This is the result. The Make modules that actually earn their place in my stack every week. And the ones I stopped using because they were just clever complexity.

Ground rules for staying in my stack

I started by setting a few brutal rules. A module or pattern had to tick at least one of these boxes to survive:

  • It removes a recurring manual task I was doing at least weekly.
  • It surfaces information I actually act on within 24 hours.
  • It is easy to debug at 23:30 when a client pings me.

If a module only existed because it was “cool” or because some YouTube tutorial said it was “powerful”, it went on the chopping block.

With that, here is what survived.

HTTP modules: boring, essential, everywhere

If Make.com removed the HTTP modules tomorrow, half my setup would die. HTTP is my default way to talk to anything that does not have a decent Make app, or has one but I do not trust it.

The ones I use every week:

  • HTTP → Make a request For hitting custom webhooks, internal tools, small APIs I host on cheap servers.
  • Webhook → Custom webhook For receiving events from my own apps and client projects.

Example from my actual stack. I have a tiny Go API that aggregates baseball practice data, sleep metrics, and a couple of biomarker logs. Make calls this API via HTTP every morning at 06:00, does a bit of mapping, and posts a summary into a private Slack channel I call #body-log.

I used to run this through three different native integrations. It broke often. I was constantly re-authing things. Replaced everything with one HTTP module, one custom webhook, and a small script on my server. Zero regrets.

Module verdict: HTTP stays. Always.

Routers + filters: my actual “no-code if/else”

I tried to avoid routers for a while. They felt heavy and visually noisy. Then I realised I was trying to fake routers with duplicated scenarios and filters at the start. That was worse.

Now I use routers and filters in almost every scenario that survives longer than a week.

Typical weekly pattern. I have a master “events in my world” scenario. It listens to webhooks from:

  • My portfolio contact form
  • Client project error hooks
  • New newsletter signups
  • Some very opinionated monitoring pings I run on my infra

One router. Four paths. Each path has a very clear filter like “type = 'contact_form'” or “severity >= 'error'”. The outputs hit different Slack channels, different Notion databases, and in one case an SMS if it is client-critical.

The important part. I stopped doing clever nested routers. One level only. If I need more logic than that, it goes into my code, not into Make.

Module verdict: Routers stay, but only one level deep.

Slack: my notification bus, not my database

Slack is my broadcast layer. Not my brain. Not my storage. If a scenario ends in Slack, I either need to react or at least see it once. Otherwise it should not go there.

Modules that run every week:

  • Slack → Create a message For system alerts, new leads, and health summaries.
  • Slack → Create a scheduled message For repeating nudges. Example: a weekly prompt to do a quick retrospective on projects and training.

One specific flow. When someone fills in a project inquiry on my site, Make receives the webhook, tags it based on budget and timeline, pushes it into a Notion CRM database, then posts a short summary to #inbox with a direct “Reply in under 12 hours” reminder tag.

I used to push every possible event into Slack. Deploy hooks. CI results. New followers. Half of that turned into noise. I killed an entire category of Slack notifications and did not miss them once.

Module verdict: Slack stays, but only for things I can act on quickly.

Notion: slow, slightly painful, still very useful

I have a love-hate relationship with Notion as an API target. It is not fast. It is sometimes weird with rate limits. But it sits in the middle of how I plan work and content, so I tolerate it.

Every week I use:

  • Notion → Create a database item Anything that should live longer than a notification.
  • Notion → Update a database item For syncing status from other tools.

One concrete example. For this blog, I draft in Markdown locally. When I push a new post into my Git repo, a small server-side hook hits Make via webhook. Make then creates or updates a Notion page in my “Content Log” database with:

  • Title
  • Slug
  • Status (Draft, Scheduled, Live)
  • URL
  • Technical notes (stack, integrations, experiments)

I used to manage that inside Notion directly with templates and properties and a whole ceremony. Now Make is the bridge from my real workflow (git, editor, deploy) into my “remember this later” tool.

What I stopped doing. I used to sync every little metric into Notion. Newsletter subscriber counts. Basic analytics. It felt like a quantified self dashboard. I never looked at it. Now I only store artefacts and decisions, not vanity numbers.

Module verdict: Notion stays, but only for durable records, not dashboards.

Google Sheets: my scratchpad and debug console

I think Google Sheets is underrated as a Make companion. Not as a database, but as a scratchpad.

Weekly modules:

  • Google Sheets → Add a row For lightweight logging and temporary reports.
  • Google Sheets → Get a cell / Get a row For quick lookups when a real database would be overkill.

Example. During a recent experiment on sleep and training volume, I patched together data from several APIs that did not agree on timestamps or time zones. Before building a nice pipeline, I sent everything to a Google Sheet first. One flat sheet. One row per event. Timestamps, scores, tags.

I used that as a human-readable debug view while I iterated on the mapping logic in Make. Once I trusted the pipeline, I replaced the sheet with a proper HTTP call into my API. The sheet scenario is still there, just turned off. It is my “turn this on for debugging” escape hatch.

Module verdict: Sheets stays as my visible logbook, not as core storage.

Text & data transformers: the unsung heroes

If a Make scenario feels complicated, it is usually because I forgot that Make has a lot of little transformer modules and functions that remove the need for code.

Every week I use:

  • Text parser and regex-related functions For pulling IDs out of URLs, cleaning labels, normalising tags.
  • Formatter → Date and time For making timestamps actually readable and consistent.
  • Array → Aggregate / Iterator For reshaping messy API responses.

One real scenario. My monitoring setup sends JSON payloads with long nested structures. Make receives them, then I use “Iterator” to walk through failing checks, clean each label with a Text function, and then aggregate them back into a compact Slack message.

I used to firehose the raw JSON into Slack. No one read it. Not even me. Now the message is human-sized and I only link to the raw payload when I actually need to debug.

Module verdict: These stay. They make other modules simpler.

The modules I stopped using regularly

Now the fun part. The tools I actively moved away from. Some of them are great on paper, just not for how I work.

Gmail & email modules as the primary trigger

I used to have a bunch of scenarios that started with “New email in Gmail” or “Watch emails”. They tried to be smart. Auto-labeling. Auto-forwarding. Auto-creating tasks.

The problem. Email is already messy. Adding an automation layer that sometimes works and sometimes does nothing makes it worse. I also do not want to give every automation direct access to my entire inbox.

What I do instead. Email is almost always the output now, not the input. If something critical happens in a system that is not wired into my attention stack, I send myself a short email. That is it.

Module verdict: Gmail triggers are out. Email stays as occasional output only.

Make Data Store as a hidden database

Make has a built-in Data Store. On paper it is convenient. Throw key-value pairs in there. Persist things. Done.

In practice, I found it dangerous. It is invisible from the outside, hard to migrate, and a bit too easy to rely on for critical data. I had one scenario where I used Data Store to track daily task limits per client. When I rebuilt that scenario a year later, I forgot the Data Store even existed and almost wiped it.

Now I keep state either in my own API, in a real database, or in something like Notion if the stakes are low. Data should live somewhere I can back up, inspect, and move without Make in the middle.

Module verdict: Data Store is out for me, except for throwaway experiments.

Scheduling heavy logic directly inside Make

I went through a phase where I tried to make Make my central scheduler. Complex recurring jobs. Custom repeat rules. User-specific offsets. You can do it. I did. It was fragile.

Make is great at “When X happens, do Y”. It is less great at being a full-on job scheduler for dozens of different rules and calendars. Especially once you start mixing time zones.

Now I keep scheduling logic in my own services. They decide when to call Make, usually via webhook. Make just does the orchestration work.

Module verdict: Advanced scheduling inside Make is out. I trust my own code for that.

Over-using search modules as if Make was my search engine

Make has lots of “Search” modules. Search rows. Search pages. Search messages. I abused them early on.

Pattern looked like this. Scenario triggers. Then: “Search X by Y” to find a record. Then update it. Worked fine at small scale. Terrible once you grow a bit. Slow. Expensive. Painful to debug when matching goes wrong.

These days I try to design flows so I always pass IDs around explicitly. When something is created, I keep the identifier and store it in the other system right away. No more “guess which row” logic.

Module verdict: Search is a fallback now, not a default.

How I decide if a new module earns a place

Every time I add a new module or app into my Make world, I run it through a simple checklist.

  • Can I explain this scenario in one sentence? If I cannot, it is already too complex.
  • Can I debug it half-asleep? If an error email at midnight would confuse future me, I simplify it.
  • Can I rebuild this without Make? If the answer is no, I am locking too much into one tool.

Make is fantastic glue. It is not my app platform. It is the thing that connects my apps, my scripts, and the buckets where I keep data.

The modules that survived this audit all share one thing. They do boring, reliable work. HTTP moves data. Routers branch logic. Slack shouts when something matters. Notion stores decisions. Sheets gives me a visual log. Everything else is optional.

If your Make account feels loud and untrustworthy, I would start there. Open your scenario list. Sort by last run. Keep what actually runs weekly and makes your life easier. Kill the rest. You will not miss as much as you think.

Building a cost-efficient LLM caching layer in Python

LLM API costs add up fast. If your application calls a language model API for every user request, you are paying for a lot of duplicate work. In many production systems, 30–50% of incoming queries are either exact repeats or semantically near-identical to something you have already answered. A caching layer captures those hits before they reach the API.

This tutorial builds a two-tier cache: exact-match via Redis (SHA-256 key) and semantic near-duplicate detection via cosine similarity over stored embeddings.

The problem in numbers

Say you are running a customer support assistant that handles 100,000 queries per day. Your LLM costs $0.01 per 1,000 tokens, and the average query + response is 500 tokens.

  • Daily cost without caching: 100,000 × 0.5 × $0.01 = $500/day
  • With a 40% cache hit rate: 60,000 API calls × 0.5 × $0.01 = $300/day
  • Monthly saving: $6,000

That 40% is conservative — real-world support bots often hit 60%+ once the cache is warm.

Architecture

User query
    │
    ▼
[SHA-256 exact match] ──hit──▶ return cached response
    │ miss
    ▼
[Embedding cosine similarity] ──hit (>0.92)──▶ return cached response
    │ miss
    ▼
[LLM API call]
    │
    ▼
[Store in both caches]
    │
    ▼
Return response

Dependencies

pip install redis openai numpy python-dotenv

You also need a running Redis instance:

docker run -d -p 6379:6379 redis:7-alpine

The exact cache (Redis + SHA-256)

Exact caching is trivial but catches a surprising volume of traffic — especially bots, retries, and repeated UI actions.

import hashlib
import json
import redis

class ExactCache:
    def __init__(self, host="localhost", port=6379, db=0, ttl=86400):
        self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True)
        self.ttl = ttl  # 24 hours default

    def _key(self, prompt: str, model: str) -> str:
        payload = f"{model}::{prompt}"
        return "llm:exact:" + hashlib.sha256(payload.encode()).hexdigest()

    def get(self, prompt: str, model: str) -> dict | None:
        raw = self.client.get(self._key(prompt, model))
        if raw:
            return json.loads(raw)
        return None

    def set(self, prompt: str, model: str, response: dict) -> None:
        self.client.setex(
            self._key(prompt, model),
            self.ttl,
            json.dumps(response)
        )

The semantic cache (embeddings + cosine similarity)

Near-duplicate detection requires converting queries to embeddings and comparing them in vector space. We store embedding vectors in Redis alongside their cached responses.

import numpy as np
from openai import OpenAI

class SemanticCache:
    def __init__(self, redis_client: redis.Redis, threshold=0.92, ttl=3600):
        self.redis = redis_client
        self.threshold = threshold
        self.ttl = ttl
        self.embed_client = OpenAI()  # or any OpenAI-compatible client

    def _embed(self, text: str) -> np.ndarray:
        response = self.embed_client.embeddings.create(
            model="text-embedding-3-small",
            input=text
        )
        return np.array(response.data[0].embedding, dtype=np.float32)

    def _cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float:
        return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

    def _all_entries(self):
        keys = self.redis.keys("llm:semantic:*")
        for key in keys:
            raw = self.redis.get(key)
            if raw:
                yield key, json.loads(raw)

    def get(self, prompt: str) -> dict | None:
        query_vec = self._embed(prompt)
        best_score = 0.0
        best_entry = None

        for key, entry in self._all_entries():
            stored_vec = np.array(entry["embedding"], dtype=np.float32)
            score = self._cosine_similarity(query_vec, stored_vec)
            if score > best_score:
                best_score = score
                best_entry = entry

        if best_score >= self.threshold and best_entry:
            return best_entry["response"]
        return None

    def set(self, prompt: str, response: dict) -> None:
        vec = self._embed(prompt)
        key = "llm:semantic:" + hashlib.sha256(prompt.encode()).hexdigest()
        entry = {
            "prompt": prompt,
            "embedding": vec.tolist(),
            "response": response
        }
        self.redis.setex(key, self.ttl, json.dumps(entry))

Note on scale: scanning all keys works fine up to a few thousand entries. Beyond that, swap the linear scan for a vector database like Qdrant or pgvector.

The unified cache manager

import time
from dataclasses import dataclass, field

@dataclass
class CacheMetrics:
    exact_hits: int = 0
    semantic_hits: int = 0
    misses: int = 0
    total_requests: int = 0
    latency_saved_ms: float = 0.0

    @property
    def hit_rate(self) -> float:
        if self.total_requests == 0:
            return 0.0
        return (self.exact_hits + self.semantic_hits) / self.total_requests

class LLMCacheManager:
    def __init__(self):
        r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
        self.exact = ExactCache(ttl=86400)
        self.semantic = SemanticCache(r, threshold=0.92, ttl=3600)
        self.llm_client = OpenAI()
        self.metrics = CacheMetrics()

    def query(self, prompt: str, model: str = "gpt-4o-mini") -> dict:
        self.metrics.total_requests += 1
        t0 = time.perf_counter()

        # Tier 1: exact match
        cached = self.exact.get(prompt, model)
        if cached:
            self.metrics.exact_hits += 1
            self.metrics.latency_saved_ms += (time.perf_counter() - t0) * 1000
            return {**cached, "_cache": "exact"}

        # Tier 2: semantic match
        cached = self.semantic.get(prompt)
        if cached:
            self.metrics.semantic_hits += 1
            self.metrics.latency_saved_ms += (time.perf_counter() - t0) * 1000
            return {**cached, "_cache": "semantic"}

        # Cache miss — call the API
        self.metrics.misses += 1
        response = self.llm_client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}]
        )
        result = {
            "content": response.choices[0].message.content,
            "model": model,
            "usage": response.usage.model_dump(),
            "_cache": "miss"
        }

        # Store in both caches
        self.exact.set(prompt, model, result)
        self.semantic.set(prompt, result)

        return result

    def print_metrics(self):
        m = self.metrics
        print(f"Total requests : {m.total_requests}")
        print(f"Exact hits     : {m.exact_hits}")
        print(f"Semantic hits  : {m.semantic_hits}")
        print(f"Misses         : {m.misses}")
        print(f"Hit rate       : {m.hit_rate:.1%}")
        print(f"Latency saved  : {m.latency_saved_ms:.0f} ms")

Putting it together

if __name__ == "__main__":
    cache = LLMCacheManager()

    queries = [
        "What is a buffer overflow vulnerability?",
        "Explain buffer overflow attacks",          # semantic near-duplicate
        "What is a buffer overflow vulnerability?", # exact duplicate
        "How does SQL injection work?",
        "What is SQL injection?",                   # semantic near-duplicate
    ]

    for q in queries:
        result = cache.query(q)
        print(f"[{result['_cache']:8s}] {q[:60]}")

    print()
    cache.print_metrics()

Sample output after the cache is warm:

[miss    ] What is a buffer overflow vulnerability?
[semantic] Explain buffer overflow attacks
[exact   ] What is a buffer overflow vulnerability?
[miss    ] How does SQL injection work?
[semantic] What is SQL injection?

Total requests : 5
Exact hits     : 1
Semantic hits  : 2
Misses         : 2
Hit rate       : 60.0%
Latency saved  : 1847 ms

Tuning the similarity threshold

The 0.92 threshold is a good starting point but you should calibrate it on your own data:

def evaluate_threshold(cache: SemanticCache, pairs: list[tuple[str, str, bool]]):
    """
    pairs: list of (query_a, query_b, should_match)
    """
    for q_a, q_b, expected in pairs:
        vec_a = cache._embed(q_a)
        vec_b = cache._embed(q_b)
        score = cache._cosine_similarity(vec_a, vec_b)
        match = score >= cache.threshold
        status = "OK" if match == expected else "WRONG"
        print(f"[{status}] {score:.3f}  '{q_a[:40]}' vs '{q_b[:40]}'")

Run this with a labelled sample of 50–100 query pairs from your actual traffic to find the right threshold for your domain.

Cost accounting

Add a simple cost tracker to quantify savings in real time:

COST_PER_1K_TOKENS = 0.01  # adjust for your model

def estimated_savings(metrics: CacheMetrics, avg_tokens: int = 500) -> float:
    hits = metrics.exact_hits + metrics.semantic_hits
    saved_tokens = hits * avg_tokens
    return (saved_tokens / 1000) * COST_PER_1K_TOKENS

For teams building AI-powered security tooling — where queries about CVEs, attack patterns, and compliance requirements tend to repeat heavily — caching can eliminate a majority of API spend. We cover patterns like this at AYI NEDJIMI Consultants when auditing AI pipelines for production readiness.

What to do next

  • Replace the linear embedding scan with Qdrant, Weaviate, or pgvector for production scale
  • Add cache warming: pre-embed your top-100 FAQ at startup
  • Track per-prompt hit rates to find eviction candidates
  • Add cache invalidation hooks when underlying knowledge changes

The full source is intentionally minimal — around 150 lines — so you can drop it into any existing codebase without pulling in a heavy framework.

How to Prove a File Existed Before a Certain Date Using Bitcoin (Without Running a Node)

The timestamp problem

You ship a file. A week later, someone claims they had the same idea first. How do you prove your file existed before theirs?

Cryptographic hashes solve “this file is unchanged.” They do not solve “this hash existed at this time.” For that you need a timestamp that somebody else can verify without trusting you.

The two usual answers are a Certificate Authority timestamp (trusts the CA) or a blockchain transaction (costs fees, requires a wallet, requires you to wait for confirmation). Both have real costs.

OpenTimestamps solves this by aggregating many hashes into a Merkle tree and anchoring the root in a Bitcoin transaction. One on-chain transaction serves tens of thousands of commitments. The cost per hash approaches zero. The trust model is Bitcoin’s own hash rate.

PowForge Witness (PFWIT) puts an HTTP endpoint in front of this pipeline.

How it works

Every 10 minutes, the PFWIT pipeline:

  1. Collects all SHA-256 hashes submitted since the last batch
  2. Builds a Merkle tree from those hashes (leaves are the raw hashes)
  3. Submits the Merkle root to four OTS calendar servers (alice.btc.calendar.opentimestamps.org, bob, finney, catallaxy)
  4. Stores the batch with each leaf’s inclusion path

The OTS calendars aggregate roots from many services and anchor them to Bitcoin roughly every hour. Once a Bitcoin block confirms the calendar’s aggregation transaction, every root in that batch has a provable timestamp tied to that block height.

For individual hashes, the proof chain is:

your_hash → Merkle inclusion path → batch_root → OTS proof → Bitcoin block header

The endpoint

curl -s https://captcha.powforge.dev/api/pfwit/certificate/YOUR_SHA256_HEX

Replace YOUR_SHA256_HEX with the 64-character hex SHA-256 of whatever you want to timestamp. The response looks like this:

{
  "pfwit_version": 1,
  "found": true,
  "hash": "a3f9...",
  "batch_id": 4,
  "merkle_root": "7e2a...",
  "leaf_index": 12,
  "merkle_proof": ["ab3c...", "11d4...", "..."],
  "anchored_at": "2026-05-23T14:03:11.000Z",
  "ots_status": "submitted",
  "ots_proof_hex": "004f...",
  "ots_download_url": "https://captcha.powforge.dev/api/witness/proof/4",
  "verify_command": "curl -s "https://..." -o proof.ots && ots verify proof.ots"
}

When ots_status is confirmed, the proof has a Bitcoin block attestation. The verify_command in the response is a copy-paste-ready command to independently verify the OTS proof using the ots CLI tool from the OpenTimestamps reference implementation.

Try it now

Pick a file. Hash it. Submit.

# Hash your file
sha256sum myfile.txt
# 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea  myfile.txt

# Submit to PFWIT (just the hex, no prefix)
HASH="3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea"
curl -s https://captcha.powforge.dev/api/pfwit/certificate/$HASH

If the hash is not yet in a batch, you will get "found": false. Submit the hash to the ingest endpoint first:

# Ingest (add to next batch)
curl -s -X POST https://captcha.powforge.dev/api/pfwit/ingest 
  -H "Content-Type: application/json" 
  -d "{"hash": "$HASH"}"

Then wait up to 10 minutes for the next batch cycle, and the certificate endpoint will return your proof.

Verify independently

The whole point is you should not have to trust PowForge to use this. Download the OTS proof file and verify it yourself:

# Install opentimestamps-client
pip install opentimestamps-client

# Download and verify
curl -s "https://captcha.powforge.dev/api/witness/proof/BATCH_ID" -o proof.ots
ots verify proof.ots

When the Bitcoin block is confirmed (1-2 hours after submission), ots verify will tell you which Bitcoin block height attests to the existence of the Merkle root, which includes your hash.

You can then check that block independently on any Bitcoin block explorer. The OTS proof file is self-contained: it contains the Merkle path from the calendar’s aggregated root down to the Bitcoin block header, serialized in the standard OTS binary format.

What this is good for

Code signing without a CA. Hash your release binary and get a Bitcoin-anchored timestamp. Nobody can backdate a timestamp that is already in a Bitcoin block.

Research priority. Hash a preprint, submit it, get the OTS proof. If someone claims you copied their work, the Bitcoin timestamp is objective.

Audit logs. Hash a database export or a log file at end-of-day and anchor it. The chain proves you had that exact data at that exact time.

Contract snapshots. Hash the text of an agreement at signing time. Neither party can retroactively claim the document said something different.

What this is not

A database timestamp that says you signed something. A notary service. A legal instrument. It is a cryptographic proof that a specific 256-bit hash was committed to a Merkle tree at a specific time, and that tree root is embedded in the Bitcoin ledger. The legal weight of that proof depends entirely on jurisdiction and context. On its own it is a mathematical fact. What that fact is worth to a court is a separate question.

The falsifier

I am filing a hypothesis: at least one publisher requests a witness retrieval via GET /api/pfwit/certificate/:hash by 2026-06-22. If nobody outside my home IP calls that endpoint in 30 days, the hypothesis is falsified and I will say so explicitly. Progress on this hypothesis is visible by checking the endpoint — if it responds, the service is live.

If you try it and it works, or does not work, I want to know. Open an issue at the GitHub mirror or reply here.

All code is MIT. The OTS proof format is an open standard. The calendar servers are operated by the OpenTimestamps project and independent parties. The pipeline runs on PowForge infrastructure at captcha.powforge.dev.

When the Dashboard Lies: The Google I/O 2026 Agent Test

The Broken Dashboard Test: problem, investigation, evidence, and human approval

This is a submission for the Google I/O Writing Challenge

It is Monday morning. A leadership meeting starts in an hour. The sales dashboard is open, the numbers look normal, and nobody is seeing a big red error message.

Then someone says the worst possible sentence:

“I don’t think this number is right.”

That is when a dashboard stops being a dashboard and becomes an investigation.

Maybe the refresh timestamp looks fine, but the source data is behind. Maybe the source system has the new records, but the report is filtering them out. Maybe the pipeline technically completed, but skipped something important. Or maybe the number is correct and the business definition changed without everyone realizing it.

That is the kind of problem I thought about during Google I/O 2026.

Google I/O had plenty of bigger moments: new Gemini models, Antigravity, managed agents, AI Studio updates, Firebase improvements, and more ways to build with agents. But the part that stayed with me was not just that agents could do more. It was that Google kept showing the pieces around the agent: tools, sandboxed execution, deployment paths, realtime data, and approval moments.

That matters for business systems.

An agent is not useful because it sounds smart. It is useful if the surrounding workflow helps it check, explain, and stop at the right time.

For me, the test is simple:

Can an agent help investigate a broken dashboard before people trust the wrong answer?

My lens is business systems

My lens is not pure software engineering. I work around business systems: reporting, Salesforce-style processes, data quality, workflow rules, and the awkward handoff between “the system says this” and “the business expected that.”

So when I watch agent demos, I do not only ask:

“Can this write code?”

I ask:

“Could this help figure out why a number is wrong without making the situation worse?”

In reporting work, I have learned to be careful with the phrase “the number is wrong.” Sometimes it means the number is stale. Sometimes it means the filter is wrong. Sometimes it means the user expected a different definition. Sometimes the data is correct, but the timing is not.

Those are different problems. Treating them like one problem wastes time.

A green checkmark on a refresh history page does not always mean the business problem is solved. A dashboard can be “working” and still be misleading.

That is why a dashboard incident is a good test for AI agents. It forces them to handle the parts of work that demos usually skip: stale data, unclear definitions, permissions, uncertainty, and stakeholder communication.

A small example: the $1.2M problem

Imagine a sales dashboard says this month’s booked revenue is $1.2M.

Sales says three new closed-won opportunities came in this morning, so the number should be higher. The dashboard refresh history says the report ran at 8:10 AM. The source system shows the new opportunities were updated at 8:26 AM. The pipeline status says the latest run completed, but with warnings.

Nothing is obviously on fire.

The dashboard loads.

The refresh did happen.

The source system has records.

The meeting still starts in an hour.

This is exactly where vague confidence is dangerous. You do not need someone to say, “Looks fine.” You need someone to narrow the problem.

Is the dashboard stale? Did the source update after the refresh? Did the pipeline skip some records? Is the report filtering out the new opportunities? Should leadership use the current number or wait for an updated one?

That is the broken dashboard test.

The Broken Dashboard Test: problem, investigation, evidence, and human approval

The manual version

In real life, these checks rarely happen in a perfect order. You bounce around. You open the dashboard, then the source system, then a refresh history page, then maybe a spreadsheet someone sent last week.

Half the work is technical. The other half is figuring out what people actually mean when they say “wrong.”

A basic dashboard investigation might look like this:

Step Manual analyst check
1 Check the dashboard refresh timestamp
2 Check whether the source system has newer records
3 Check if the pipeline, export, or sync completed
4 Compare row counts, timestamps, or sample records
5 Look for filters, joins, or definitions that could explain the gap
6 Identify which reports, teams, or decisions may be affected
7 Write a plain-English update for stakeholders

The hard part is switching between tools and contexts while people are waiting. You are trying to separate facts from guesses. You are also trying not to create more panic than necessary.

That is where the Google I/O agent story became useful to me.

The agent version should be boring on purpose

The part of Google I/O 2026 that clicked for me was the move away from “ask a model a question” toward “give an agent a job, tools, and boundaries.”

A model can help explain what a stale dashboard might mean. An agent, designed carefully, could help check the refresh time, inspect a pipeline status, compare a few sample records, and produce a short recovery note for the humans involved.

But the agent version should not be:

“Let the AI fix it.”

That is how you get a bigger problem.

The useful version is more controlled: let the agent organize the investigation, gather evidence, and prepare a recommendation. Then a person decides what to do.

For a dashboard incident, I would want an agent to summarize the issue, identify the systems involved, gather freshness evidence, compare source records against dashboard output, estimate the blast radius, draft a recovery brief, and ask for approval before any risky action.

That last point is not decoration. It is the line between useful automation and reckless automation.

If an agent finds a failed refresh, it can recommend a rerun. If it sees a mismatch, it can flag the likely cause. If it drafts a message, great. But changing production data, editing report logic, disabling a process, or triggering a major workflow should require a human decision.

The goal is faster evidence, not blind autonomy.

Why the Firebase demo mattered to me

One I/O moment that fit this idea surprisingly well was a Firebase SQL Connect realtime demo. The demo itself was playful, but the pattern was familiar: data changed, the app did not immediately show the change, and then realtime sync improved how updates reached the user.

That is basically the dashboard problem in miniature.

Most business users will not describe the issue as a sync problem, a refresh issue, or a data modeling mismatch. They will just say:

“This looks wrong.”

A good agent workflow should help translate that vague sentence into checks that can actually be tested:

  • When did the dashboard last refresh?

  • Did the source system receive newer records?

  • Did the pipeline run fully or only partially?

  • Are the missing records actually missing, or filtered out?

  • Who needs to be warned before the meeting?

The value is in turning a vague complaint into checks someone can review.

The recovery brief matters too

The investigation is only half the job; the other half is explaining it without dumping technical noise on people who just need to know whether they can trust the number.

A useful recovery brief should explain what happened, what is confirmed, what is still uncertain, who may be affected, what action is recommended, and when the next update will come.

That sounds simple, but under pressure it is easy to write too much, too little, or the wrong thing entirely.

This is an underrated agent use case. If the agent can gather evidence and draft a calm first version, the analyst can spend more time judging the situation instead of starting from a blank message.

It gives a person something to review.

My caution: investigation and action are different jobs

This is where I get cautious.

A dashboard investigation agent should not have the same permissions as an admin. It should not be able to quietly rewrite report logic, change production data, or trigger a workflow just because it found something suspicious.

Investigation and action are different jobs.

I do not want an agent with admin-level access just because it makes the demo smoother. Convenience is not a permission model.

For this kind of workflow, I would rather start with a read-only agent that can collect evidence, summarize findings, and recommend next steps. If a write action is needed, make the approval explicit.

Read-only first is not a limitation. It is how trust gets built.

Before putting an agent anywhere near a business systems workflow, I would ask these questions first:

Question Why it matters
Is the workflow repeatable? Agents work better when the process has structure
Are the systems clearly named? Vague tools create vague investigations
Can the evidence be verified? Confidence is not the same as proof
Are permissions scoped? The agent should not reach everything
Are write actions approval-gated? Investigation and action need different levels of trust
Is there an audit trail? People need to understand what happened later
Can a non-technical stakeholder understand the output? Raw logs are not a recovery update
Is there an escalation path? Some problems should leave the agent’s hands

The worst version of this agent would be one that confidently guesses. The best version would be one that carefully narrows the search.

Final takeaway

My biggest Google I/O 2026 takeaway is not “agents are powerful.” We already know that.

The harder question is whether they can be useful when the situation is messy, time-sensitive, and full of half-truths.

For me, the test is simple:

Can the agent help move from panic to evidence?

If a dashboard lies, I do not need an agent that sounds confident. I need one that checks the right places, shows what it found, explains what is still uncertain, and stops before doing something risky.

That is the agent I would trust: not the one that replaces judgment, but the one that helps people reach evidence before decisions get made.

🚀 React Hooks & Rendering — A Clear Mental Model (For My Future Self Too)

🪝What are React Hooks?
Hooks are helpers that let a function component:
remember data
react to changes

🧠 Core React Hooks (WHAT + WHY)

🔹 useState — data that affects the UI
Why it exists:
React re-runs components often. Without state, values would reset every time.
Use when:
The user should see the change.

🔹 useEffect— side work after render
Why it exists:
Some work should happen after the UI is painted, not during it.
Used for:
API calls
timers

🔹useRef — remember without re-render
Why it exists:
Sometimes React needs to remember something without updating the UI.
Two main uses:
DOM access → focus input, scroll, measure
Silent storage → timer IDs, previous values, flags

🔹 useContext — shared/global data
Why it exists:
To avoid passing props through many layers.
Used for:
logged-in user
theme

🔹 useMemo — avoid heavy recalculations
Why it exists:
React re-runs components often → slow calculations can repeat unnecessarily.
Use when:
calculation is expensive

🔹 useCallback — stable functions
Why it exists:
Functions are recreated on every render.
Used mainly when:
passing callbacks to optimized child components

🌍 Rendering Types (WHY they exist)

🔹 CSR — Client Side Rendering
What: Browser builds the UI
Best for: dashboards, internal tools

🔹 SSR — Server Side Rendering
What: Server sends ready HTML
Why: Search engines need content

🔹 SSG — Static Site Generation
What: Page built once at build time
Why: Some content rarely changes

Hope it helps someone else too

Publishing a reusable React UI package as an npm module

Publishing a reusable React UI package as an npm module is one of the easiest ways to maintain consistency across multiple apps.

Here’s a simple workflow that works well:

1) Structure your package properly
Keep a clean setup like:

src/ → components + exports
dist/ → compiled build output
index.ts → single entry for exports

2) Build it in a production-ready way
Before publishing, generate:

  • ESM build (.mjs)
  • CommonJS build (.js)
  • TypeScript types (.d.ts)

Example build command:

npm run build

3) Publish it to GitHub Packages
Setup your registry in .npmrc:

@your-scope:registry=https://npm.pkg.github.com
//https://lnkd.in/dcqNXmcg

Then publish:

npm publish

4) Use beta prereleases for safe testing**
This is the cleanest way to share updates early without breaking stable users.

npm version prerelease –preid=beta
npm publish –tag beta

✅ Stable users keep installing latest
✅ Test users install beta builds
✅ You can iterate faster without risk

This release pattern is simple, scalable, and keeps upgrades controlled.

Brave CMS – A CMS Built for Simplicity, Flexibility, and Scalability

In a world filled with bloated content management systems and overly complex admin platforms, developers often search for a solution that balances simplicity, performance, customization, and maintainability. That is where Brave CMS comes in.

Brave CMS is an open-source modern content management system built with Laravel, MySQL, and Bootstrap 5, designed for developers and businesses that want a clean, extensible foundation for building websites, blogs, and online newspapers.

Rather than trying to be everything for everyone, Brave CMS focuses on excelling at the fundamentals: content management, role-based access control, security, customization, scalability, and developer experience.

Built with Laravel, MySQL, and Bootstrap 5

One of the biggest strengths of Brave CMS is the technology stack behind it.

The project is built with Laravel, MySQL, and Bootstrap 5.

Laravel serves as the backbone of the platform, bringing with it a clean MVC architecture, elegant syntax, powerful routing, authentication systems, middleware support, and an excellent developer experience overall.

Instead of dealing with messy legacy code or outdated patterns, developers work inside a structure that feels organized and maintainable from the start.

Because Brave CMS is powered by Laravel, adding new features is easy.

For the database layer, the project uses MySQL, which remains one of the most trusted relational database systems in the industry. It is fast, stable, widely supported, and perfectly suited for content-driven applications.

On the frontend side, Bootstrap 5 provides the UI foundation. The admin panel is clean, responsive, and modern without becoming visually overwhelming. Bootstrap also makes customization easier, especially for developers who want to rapidly adapt layouts or redesign parts of the interface.

The result is a CMS that feels lightweight and modern while still giving developers the structure needed for long-term projects.

Enhanced Security Features

Security is one of the areas where Brave CMS tries to take a more modern and responsible approach.

A lot of older CMS platforms became difficult to maintain over time because they relied heavily on outdated plugins, inconsistent code quality, or insecure third-party extensions. Brave CMS avoids much of that complexity by building on Laravel’s mature ecosystem and security-focused architecture.

Laravel already provides strong protection against many common vulnerabilities, including CSRF attacks, SQL injection attempts, and insecure password storage. Brave CMS benefits directly from those protections while keeping the codebase structured and predictable.

Authentication and route protection are handled through Laravel’s middleware system, which makes access control cleaner and easier to manage. Validation and sanitization are also built into the workflow, helping reduce the chances of malformed or malicious input becoming a problem.

But security is not only about technical protection.

A secure application is also one that developers can actually understand and maintain. Overly complicated systems often become insecure simply because nobody wants to touch them anymore.

Brave CMS keeps things organized and transparent, which makes long-term maintenance significantly easier.

Easy to Customize

Customization is one of the most important qualities of a modern CMS.

Many platforms promise flexibility but eventually trap developers inside rigid architectures or endless plugin dependencies. Brave CMS takes a different approach by giving developers a clean foundation that can evolve naturally depending on project requirements.

Since the platform is built on Laravel, developers can easily extend almost every part of the system. Whether it is creating new routes, building custom admin functionality, integrating external APIs, adding new content structures, or modifying frontend templates, the architecture stays approachable and easy to work with.

This flexibility makes Brave CMS suitable for far more than simple blogs.

It can serve as the base for company websites, content platforms, admin dashboards, publishing systems, or fully customized web applications.

The Bootstrap 5 frontend also helps significantly when it comes to UI customization. Developers can quickly adapt layouts, components, spacing, colors, and responsiveness without needing to redesign everything from scratch.

Instead of locking developers into a rigid ecosystem, Brave CMS gives them room to build.

Designed to Scale

A good CMS should not only work for small projects.

It should also be capable of growing alongside the application.

Brave CMS was designed with scalability in mind, largely because it inherits many of Laravel’s strengths. The framework already supports caching systems, queues, modular application structures, API development, and performance optimization tools.

That means developers can start with a relatively small project and gradually expand functionality without needing to rebuild the platform later.

As traffic and complexity increase, Brave CMS can integrate with tools like Redis caching, queue workers, CDN services, and cloud-based hosting environments. The clean separation between backend logic, frontend rendering, and database structure also makes maintenance easier as projects grow.

Scalability is not only about handling more users.

It is also about keeping the codebase manageable over time.

One of the biggest frustrations developers face with older CMS platforms is technical debt caused by years of patchwork plugins and inconsistent architecture. Brave CMS avoids much of that by embracing modern Laravel development principles from the beginning.

Theme Support

Another important feature of Brave CMS is theme support.

If you are a UI designer/developer looking for a way to turn your HTML templates into database-powered websites, Brave CMS is perfect for you.

Instead of rebuilding the frontend structure every time, developers can create reusable themes and adapt them for different purposes.

The active theme can be easily set from the admin panel’s Settings section. That means that making a new theme and using it has zero downtime.

Comprehensive Documentation and Guides

Even the best software becomes frustrating if the documentation is poor.

One of the practical strengths of Brave CMS is that it includes a comprehensive guide designed to help developers understand installation, configuration, customization, and theme development.

Instead of spending hours digging through outdated forum posts or incomplete tutorials, developers can follow a more structured workflow and understand how the platform is intended to work.

This is particularly valuable for developers who are newer to Laravel or transitioning from older CMS ecosystems.

Clear documentation lowers friction, reduces setup time, and makes customization far less intimidating.

For long-term projects, that matters just as much as the code itself.

Final Thoughts

Brave CMS represents a modern approach to content management.

Instead of overwhelming developers with unnecessary complexity, it focuses on clean architecture, strong foundations, customization, scalability, and practical usability.

Built with Laravel, MySQL, and Bootstrap 5, the platform combines modern backend engineering with responsive frontend design.

Its enhanced security structure helps developers build safer applications, while its flexible architecture allows projects to evolve naturally over time.

The addition of theme support and comprehensive documentation further strengthens its position as a developer-friendly CMS solution.

For developers looking for a Laravel-powered CMS that is clean, scalable, customizable, and grounded in modern development practices, Brave CMS is a compelling option worth exploring.