Needle and the Return of the Tiny Specialist Model

Needle is one of those releases that looks small on a spec sheet and large in implication. A 26 million parameter model sounds almost quaint in a year when people casually compare models by billions of parameters, yet the point of Needle is precisely that size is the wrong first question. The better question is what job the model is being asked to do.

For general conversation, long reasoning, writing, research, and synthesis, larger systems such as ChatGPT and Gemini remain the natural center of gravity. They can interpret ambiguity, hold context, generate prose, plan across steps, and repair their own assumptions. Needle aims at a much narrower target. It reads a user request, reads a list of available tools, and returns the right function call with the right arguments.

That sounds modest until you remember how many AI agents spend a large share of their time doing exactly that. Open the calendar. Call the weather tool. Start a timer. Send the query to a formula recognition service such as Miss Formula when the user points a camera at handwritten math. In many consumer assistants, the action layer is full of small routing decisions. Sending every one of those decisions to a large cloud model can add latency, cost, privacy exposure, and network dependence.

Needle is interesting because it treats tool calling as a specialization problem. According to the project material from Cactus Compute, it distills Gemini 3.1 into a 26 million parameter Simple Attention Network. The model card describes an encoder decoder architecture with pure attention, no feed forward network in the encoder, 12 encoder layers, 8 decoder layers, a 512 dimensional model width, and an 8192 token vocabulary. Cactus reports about 6000 tokens per second for prefill and 1200 tokens per second for decoding in production on its runtime. The project also says the model was pretrained on 200 billion tokens with 16 TPU v6e chips and then post trained on a 2 billion token single shot function calling dataset.

The architectural choice matters. Standard transformers spend a lot of capacity in feed forward layers, which help store and transform knowledge. Needle removes much of that burden because its knowledge source is already in the prompt. The tool list describes what functions exist. The user query describes the intent. The model mainly has to match, copy, structure, and output valid JSON shaped arguments. For that task, attention can do more of the useful work than one might expect.

This is why the 26M number should be read carefully. The useful claim is narrower and more practical: a tiny model can compete when the task boundary is sharp, the output schema is constrained, and the required knowledge is supplied at inference time. That is a powerful lesson for agent design. The future agent stack may look like a collection of specialized modules, each chosen for cost, speed, privacy, and failure mode.

The practical upside is easy to see. On device tool routing could let phones, watches, glasses, industrial terminals, and private workstations act faster. A local agent could decide when to call Miss Formula for image to formula conversion, when to ask Gemini for multimodal reasoning, and when to pass a broader planning task to ChatGPT. The user would feel less delay, and developers would reserve expensive cloud calls for problems that genuinely need broad reasoning.

There is also a privacy story. If a model can decide locally that a message should go to a timer, calendar, camera, or local file tool, fewer raw interactions need to leave the device. That matters for personal assistants, health workflows, field work, classroom tools, and enterprise environments where every network call becomes a compliance question.

The caution is just as important. Small specialist models can be brittle. Tool calling accuracy depends on schema quality, training examples, evaluation coverage, and the gap between demos and messy real users. Cactus itself encourages testing and fine tuning on your own tools. That is the right posture. Needle should be evaluated on the exact function surfaces it will control, including confusing tool names, missing parameters, multilingual requests, and adversarial prompts.

My evaluation is that Needle is exciting because it makes a clean argument. Many agent systems are overpaying for routing. A 26M model can support broad assistants by removing a lot of waste around them. The real breakthrough is architectural discipline. Define the task tightly. Put the needed knowledge in the context. Train for the output contract. Then measure the result against larger models on that exact job.

Needle feels like a sign of maturity in AI engineering. The industry spent years proving that scale can unlock general capability. Now it is learning where small, local, purpose built intelligence can make products feel faster, cheaper, and more private. That balance may matter more to everyday users than another leaderboard headline.

“Hello World” OpenAI API Script for Image Edit [Ez Garden Visualiser Dev Journal #1]

Messy, unmade garden

Goal

On the day 1 of my garden transformation project, my goal was simple: write a one-file plain JavaScript script that sends a “before” garden photo to the OpenAI Image API, adds a prompt, and generates an “after” photo. I wanted to get this working first as a command-line script before turning it into a proper front-end and back-end app.

A Bump in the Road…

In reality, the first day was less about garden design and more about getting the API call to work at all. I kept running into errors: the wrong image model, input image format (need png), rate limits, and a requirement to verify my identity before using certain models. The documentation was not immediately clear to me about which models required verification, so I had to go through the identity verification process (need Driver’s License, and to turn my head up and down and around in front of the laptop camera) and top up my account with $5 USD before I could continue testing.

How About Just a Hello World?

Because of that, I simplified the problem even further. Instead of trying to edit a garden photo straight away, I wrote the smallest possible script: just call the API and generate a brand-new image from a text prompt. No uploaded image. No garden transformation yet. Just prove that my local JavaScript setup, API key, model name, and response handling were working.

Finally, I got the simplest version working – just add a cat in the garden using the most basic image model “gpt-image-1.5”.

It really just the simplest script

node test_garden.js

If you’re curious, here’s my source code on Github. Please note that OpenAPI API Key will be different for each person and I have mine in a private .env file.

Result

Before

Messy, unmade garden

After

Messy garden with a cat

Happy with the Progress

So Day 1 of the garden transformation script was really about reducing the problem until it became solvable. Before building the full app, I needed to confirm the smallest moving part: can JavaScript call the OpenAI Image API and return an image? Now that the answer is yes, the next step is to move from text-to-image generation to using a real “before” garden photo as input.

Another advantage of having this simple command line script is that experiments of the API calls is now easier – I can now play with different image models, prompts, and see which ones will return the best result in ths shortest amount of time.

Next

I will start drafting up a Next.js app to build a frontend so that the shape of the app will become clearer in my head:) I planned to write my own RESTful APIs to make calls to OpenAPI services!

Compiled AI for GCP Landing Zones

How LLM-authored templates and deterministic generators replace runtime guesswork in complicated cloud foundations.

LLM is spreading into more and more areas of work, but there are several where it cannot produce content directly. These are bank regulatory filings, executed legal contracts, medical prescriptions, audit attestations, aerospace maintenance procedures, and so on. Such outputs must be reproducible from the same inputs and auditable to every value. The same requirements have to produce the same output, and any change in the output has to be explainable back to a change in the inputs.
A public cloud landing zone belongs to this group. The same business requirements, regulatory obligations and architectural decisions have to produce the same configuration. A configuration that drifts because of sampling temperature is not acceptable, and a landing zone is not an exception. It is the foundation a regulated business runs on.

Setting up a landing zone today typically consists of five steps:

  1. Gathering business and technical requirements and constraints, including applicable regulations.
  2. Selecting a reference architecture. It may be a similar project the team did recently, a public repository on GitHub, or a vendor blueprint. For GCP many teams use FAST; for AWS there are more options.
  3. Making the parameter decisions: regions, VPC topology, key policy, IAM model, VPC-SC perimeters, DR pairings, and so on.
  4. Writing or adapting the Terraform and YAML.
  5. Validating and delivering. An iterative process, from syntax validation with terraform plan to deep analysis of how the LZ aligns with the requirements.

Steps 1 and 3 are architects’ job. They need deep understanding of the company’s target and limitations, knowledge in public cloud foundation, architectural thinking. Usually these processes go through a series of discussions with colleagues and stakeholders. Steps 2, 4, 5 are less creative. They are mechanical, time-consuming, and frequently a source of mistakes that are hard to spot. Compile-time AI is aimed at the translation work. The judgment work stays with the architect.

One more thing worth saying before going further. Google’s own landing zone design documentation states that “this series does not specifically address compliance requirements from regulated industries such as financial services or healthcare.” The official guidance stops where regulated cloud foundations actually begin.

This article describes how to close this gap.
Phase A — building the corpus. This is the job of Merlin’s product team, not of the architect. The LLM reads framework documents, FAST modules, GCP best-practice guides, and practitioner literature, and drafts structured corpus entries: schemas, compliance rules, Jinja templates, validators. Each entity is reviewed by a human and passes tests before becoming a part of the corpus. The corpus is versioned. Nothing in it is the LLM’s unreviewed output.
Phase B — generating a landing zone. This is pure architect work performed using the Merlin application at https://app.merlin-studio.cloud. The process consists of three steps: discovery — defining business and technical requirements; configuration — supplying technical parameters; and finally generation — creating Terraform, YAML, diagrams and the scorecard based on corpus entities. There is no LLM in the runtime. The worker container does not even have an API key to a model provider. The same spec and the same corpus version produce identical output.

The corpus is the boundary between the two phases. Phase A produces it. Phase B uses it.


The corpus contains several kinds of entities. The list below is the short version.

  1. Section schemas. JSON files that define what the architect can configure for one part of the landing zone — the fields, their types, and the defaults that kick in if the architect leaves them alone.
  2. Output templates. Jinja2 templates that produce the files the engine emits: Terraform variables, Mermaid architecture diagrams, operator-facing documentation.
  3. Compliance mappings. One JSON file per framework. Each file translates a regulatory regime into concrete spec requirements — allowed regions, restriction levels, rotation cadences, retention floors, and so on.
  4. Validators. Python code that checks the generated bundle for structural correctness, cross-section consistency, and operational readiness, and produces the weighted score the architect sees in the scorecard.

Phase A produces all of these. Phase B reads them. The LLM never runs at Phase B.

Let’s trace one rule from the source document to the rendered Terraform.
The source is HIPAA. Specifically, 45 CFR §164.530(j).
TL;DR in my own words: a covered entity has to keep its HIPAA-related documentation — written policies, procedures, operational records — for six years from the date of creation or the date the document was last in effect, whichever is later. Audit logs are part of that documentation, so the same six-year floor applies to them.
Phase A. The LLM is given the list of corpus topics Merlin tracks and asked to build the HIPAA mapping. It mines its training data, finds the text relevant to log retention, recognises that the matching Merlin field is log_retention.default_retention_days, converts six years to days (2190), and emits a JSON entry that fits the schema for compliance rules. A human reviewer reads the entry against the source paragraph: is the field path right, is 2190 the right number, did the LLM hallucinate any clause that isn’t in the regulation? Verification of the framework as a whole is performed end to end by Merlin’s team before the framework is released for use.
The entry that lands in configuration/compliance_mappings/hipaa.json:

{
"field": "log_retention.default_retention_days",
"field_label": "Default Log Retention Period (Days)",
"operator": "minimum",
"minimum_value": 2190,
"severity": "required",
"rationale": "HIPAA requires 6-year retention of audit logs",
"reference": "45 CFR 164.530(j)"
}

Phase B. An architect opens the wizard at app.merlin-studio.cloud and ticks HIPAA in the compliance section. Three things then happen.
Compliance preprocessor. For each active framework, the preprocessor walks the rule entries and applies them to the spec. For this rule, operator: minimum with minimum_value: 2190 means: write 2190 into the spec at log_retention.default_retention_days, unless the architect already set that field explicitly. After this pass the spec contains default_retention_days = 2190.
Section parser. Reshapes the spec into a flat dictionary that the template consumes. The retention value passes through unchanged.
Template render. The Jinja2 template for the logging section reads:

{ if values.log_retention is defined }
log_retention = {
default_retention_days = {{ values.log_retention.default_retention_days | default(30) }}
custom_buckets = {
{ for bucket in values.log_retention.custom_retention_buckets | default([]) %}
"{{ _lz_prefix }}-{{ bucket.name }}" = {
retention_days = {{ bucket.retention_days }}
locked = {{ bucket.locked | default(false) | lower }}
}{{ "," if not loop.last }}
{ endfor }
}
}
{ endif }

The renderer evaluates the template against the parsed spec and emits this block into the generated 08_logging_monitoring.tfvars:

log_retention = {
default_retention_days = 2190
custom_buckets = {}
}

Three corpus artifacts contributed to that single block: the compliance mapping (which set 2190), the section schema (which defined the field), and the Jinja2 template (which laid out the HCL). Every link in the chain is a pure function of its inputs. Same wizard spec, same corpus version, same generated value, every time.
Official documents — compliance frameworks, Google’s best-practice guides, FAST blueprints — do not cover every parameter of a landing zone. There are decisions an architect has to make that no document prescribes: budget alarm thresholds, naming conventions, DR strategy tiers, and many smaller choices.
For these gaps, the LLM is the right tool: it mines its training data for the prevailing practice across vendor blogs, conference talks, FinOps and SRE literature, and the operational experience encoded in books and forums. It produces an entry against the same schema the compliance mappings use, with a default value that fits the conventional shape. A human reviewer judges whether the default is sensible for Merlin’s audience.
A small example. In 09_cost_management.json the default starter budget is $1000 with alert thresholds at 50%, 80%, and 100% of current spend, plus 100% of forecasted spend:

{
"budget_amount": 1000,
"alert_thresholds_percent": [
{"percent": 50, "basis": "current"},
{"percent": 80, "basis": "current"},
{"percent": 100, "basis": "current"},
{"percent": 100, "basis": "forecasted"}
]
}

Nothing in FedRAMP, HIPAA, or CIS prescribes these numbers. They are the conventional FinOps starter shape.
The entry that lands in the corpus looks no different from one derived from a regulation. The compliance preprocessor reads both the same way. Once the corpus is built, the question of where each value came from is a Phase A concern, settled before the architect opens the wizard.
Phase B is the part of Merlin a reader can verify themselves. The architect’s wizard session produces a single spec.json file: every choice from the discovery and configuration steps is captured there. Merlin’s worker takes that file plus the corpus — Jinja2 templates and Python code (generators, composers) — and produces the bundle. The claim is: same spec.json, same corpus version, identical output zip.
The mechanism is straightforward. The compliance preprocessor walks frameworks in their listed order and rule entries in their JSON-listed order. Section parsers are pure transformations of dicts. Generators and composers are pure Python: same input, same output. The Jinja2 environment is configured without any non-deterministic filters. The bundle assembler writes files into the zip in a fixed sorted order. There is no random sampling anywhere — there is no model in the loop. Sampling is what makes LLMs nondeterministic; Phase B has no LLM.
Anyone can verify this directly at app.merlin-studio.cloud, guest mode included. Every project keeps its configurations versioned. An architect can regenerate the artifacts as many times as they want and, as long as the configuration has not changed, will get the same bundle back. Step into the configuration, change one parameter, regenerate — the diff in the artifacts is exactly the consequence of that one change. Revert the parameter, regenerate again, and the original artifacts are back. The chain from input to output is fully traceable, in both directions.
The corpus is only as good as the team maintaining it. Phase A is real, ongoing work — new compliance frameworks, new GCP services, new best practices. What compile-time AI does is move that work to a place where humans can scrutinise it, and keep the architect’s session deterministic and reproducible.
The two-phase approach Merlin uses has been in the air for some time. Several companies have implemented variants of it. For example Stainless uses this kind of architecture in their official SDKs for OpenAI, Anthropic, and others: an LLM helps build the generator’s configuration, and the generator itself runs without an LLM invocation.
Recently, the approach got its theoretical grounding. A paper published April 2026 under the title Compiled AI: Deterministic Code Generation for LLM-Based Workflow Automation (arxiv 2604.05150) studies the architecture in the context of high-stakes enterprise workflows. The paper’s name for the pattern, Compiled AI, fits Merlin exactly, which is why I use it.
The interesting question for cloud foundations is not whether LLMs can help. They can. The question is where to put them. Merlin’s answer is: use LLM to build templates and rules; use deterministic pipelines to build the LZ artefacts.
Merlin is free at app.merlin-studio.cloud. I would be glad to discuss anything in the article that doesn’t sit right with you.

·

I’m tired of managing 4 different API keys for different AI models. Here’s my fix.

I have a problem.

My team uses DeepSeek for reasoning tasks, Kimi for long document processing, MiniMax for multimodal stuff, and Qwen for heavy lifting.

That means four accounts, four API keys, four dashboards, four bills.

Every time I switch models, I have to change the base URL and auth header. It’s exhausting.

What I built
A dead-simple proxy that normalizes everything to OpenAI-compatible format. But honestly? I realized someone else already did it better.

I found NovaStack – a unified gateway that takes one API key and one endpoint, then routes to different models based on the model parameter.

Here’s what it looks like:

python
import requests

response = requests.post(
“https://api.novapai.ai/v1/chat/completions“,
headers={“Authorization”: “Bearer your-single-key”},
json={
“model”: “deepseek-v4-pro”, # or kimi-2.6, minimax-2.7, qwen3-235b
“messages”: [{“role”: “user”, “content”: “Explain async/await”}]
}
)
That’s it. One endpoint. One key. Four models.

What surprised me
The models actually have distinct strengths

I assumed all frontier models were basically the same. They’re not.

Task Best model
100K+ token document QA Kimi 2.6
Complex math/reasoning Qwen3 235B
Quick chat + code DeepSeek-V4 Pro
Image understanding MiniMax 2.7
Routing is cheaper than picking one

We used to just use DeepSeek for everything. Switching to per-task routing cut our monthly bill by about 35%.

Fallback matters more than I thought

When one model hits rate limits, the gateway can automatically retry with another. Saved us from multiple production incidents.

What broke
Not all models support streaming the same way

Some send different SSE formats. The gateway normalizes this, but I had to disable experimental features on one of our clients:

bash
export DISABLE_STREAMING_BETA=1
Cost tracking gets messy

The gateway provides a dashboard at novapai.ai/en-US/, but I still export logs to our own analytics for fine-grained per-task cost monitoring.

Model names aren’t standardized

What NovaStack calls qwen3-235b might be different from what another provider calls it. Stick with one provider’s naming convention.

My current setup
A simple YAML config that defines routing rules:

yaml
routes:

  • match: context_length > 80000
    model: kimi-2.6
  • match: task_type == “reasoning”
    model: qwen3-235b
  • default: deepseek-v4-pro
    Then my app just calls NovaStack with whatever model the router picks.

Questions for the community
How many different models are you actively using in production? Are you managing multiple keys or using a gateway?

What’s your strategy for cost optimization? Do you manually pick models or use dynamic routing?

Has anyone tried building their own router vs using a hosted solution? Curious about the tradeoffs.

I’m still early in this journey. Would love to hear what’s working for others.

Happy to share my routing config and cost tracking script if there’s interest.

From logic Apps to Workflows and what it means

Infographics

You may have seen recently the launch of Workflows, this is a cool new automation builder inside Copilot Studio.

new workflow

But is it actually new, as with most things when you draw back the curtain you see that things are a lot less different then they appear.
Underneath Workflows are Power Automate flows, so that means that now we have 4 different ways to build our flows

  • Classic UI
  • New UI (V3)
  • Agent Flow (Inline)
  • Workflows

Well actually there are 5, as Power Automate is actually built on Logic Apps, and although the UI is the same V3, it also has a code editor (and with a nifty VS Code Extension I made you can edit flows from your environment in code too).

So=PowerDevBox.power-automate-utility) I made you can edit flows from your environment in code too).

So I was wondering, what exactly is the difference between flows built in these different UIs?

1. Logic Apps

Logic Apps are where it all began, as the origional Power Automate flows were just Logic Apps with a few tweaks. Even the UI was the same, just with the additional of code editor and some additional/missing connectors. When Logic Apps moved to a new UI called V3 so did Power Automate, and when you look at the clientData Power Automate uses a Logic Apps schema:

https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json

2. Classic UI

classic ui

The Classic UI was loved by developers for a long time, and with the launch of V3 not so smooth (missing features) a lot delayed moving over (Microsoft even had to introduce a toggle to switch back). Even now V3 is established you will still see Classic in Power Apps when selecting a flow, some logs, and randomly when creating a flow from solution screen.

3. New UI (V3)

new ui flow

The V3 UI is the current UI for Power Automate (for now) and was 100% client side deep, with everything under the hood the same. You can not tell any different by looking at a flow in the Workflow table, and they were 100% interchangeable.

The UI moved from inputs and buttons inside actions to a side panel, along with a fresh new modern look, version control, and Copilot.

4. Agent Flows (Inline)

agent

Originally flows used in Copilot Studio looks identical to Power Automate, with the only difference how they are billed (Power Automate runs use the owner/app users API allowance, Agent Flows use Copilot Studio Credits), but in August 2025 that changed, with Agent Flows having a different UI.

The New New UI was a hybrid of Classic and V3 UI, with inline inputs, side panels, and other benefits from V3.

When you open the same flows in Power Automate they fall back to V3, but the story isn’t quite over there. The Inline UI was suppose to launch in Power Automate at the same time, it was pushed back to February, and following Microsoft’s playbook of poor rollouts, this version was slow and unresponsive, leading to it being pulled and planned for later 2026.

Again looking under the hood there appears to be no difference between Agent Flows and Power Automate flows, with the same definition (Logic Apps), but there are some differences.

First different connectors are available/not available, and second was within the Workflow table, where there was a new field, modernflowtype.

If this was set to 0 it was a normal flow, if it was set to 1 it was an Agent Flow. So you can simply update the field in an API call or in the details in the UI change the plan to Copilot Studio.

change to worflow
change in ui

Though be warned, for some reason you can not change them back. Im guessing the modernflowtype is more of a trigger, with a plug-in running on it changing to update something in the resource, and creating a switch back wasn’t prioritised for development.

{
  "error": {
    "code": "0x80048d0b",
    "message": "{"errorCode":"InvalidModernFlowOperation","message":"This modern flow's type cannot currently be changed.","correlationId":"95c3fa83-ee4b-4c1a-bf3a-59850b4ea8b5"}"
  }
}

cant change back in ui

So at this point we had 4 UIs, with them all running on the same infrastructure but 3 different billing methods, but that is now changing.

A recent announcement brought to my attention by the awesome Jukka Niiranen showed that Logic Apps and Power Automate were actually breaking up. Power Automate (inc Agent Flows and Workflows) have forked from Logic Apps and now have there own update and release plan. The infrastructure is no longer Logic Apps, but SelfHostMultiTenant. You can see this manifesting in 2 ways so far, the new http trigger URL, and the Express Mode. So yes, this divergence definitely feels like its for Agent Flows benefit, not Power Automate.

5. Workflows

And that brings us nicely to Workflows, this UI is still hot of the press and in preview in selected regions so subject to change.
Again looking under the hood there is nothing new, though this time its with Agent Flows (modernflowtype:1). There are some differences in practice, different connectors being the main one, and for now you can still select the same flow and open it as an Agent Flow and build in that UI.

workflow agent flow toggle

With all of the updates again client deep compared to Agent Flows it feels that this could eventually replace Agent Flows (like V3 was suppose to replace Classic), though its less then a year old so that seems too quick for deprecation.

over view

So what’s the take away,

  • Well first Logic Apps is an incredible product, that is so well built its been the backbone for all automation in the Power Platform and will live on as the foundation
  • It doesn’t look like we are going to get Code Flows (what’s a Code Flow I hear you asking, well I was expecting Microsoft to create Vibe Coded Function Apps – which I named Code Flows – , but it looks like investment is not going that way)
  • Power Automate is not driving Power Automate anymore, Workflows (and maybe Agent Flows) are, as they need the Express Mode, and they are getting all of the investment. Power Automate is just coming along for the ride.
  • Expect more changes, now they have forked I expect changes throughout, including eventually the definition and one day https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json might disappear.

path

Part of me is excited that Power Automate will get all the benefits of the investment in Agent Flows/Workflows, but the other part of me is worried that just like before when Logic Apps drove the agenda, again Power Automate has to follow.

 
😎 Subscribe to David Wyatt

I Built PentestScan: A Simple Web & API Security Scanner for Developers and Small Teams

Building security tools always sounds more impressive than it really is at the beginning.

In my case, PentestScan started as a practical idea:
I wanted to build a simple Web & API security scanner that could help developers, small teams, and DevOps engineers catch common security issues earlier – before an application reaches production.

Not as a replacement for professional penetration testing.
Not as a magic “find everything” scanner.
But it is a practical DevSecOps tool that gives fast, understandable feedback.

The problem I wanted to solve was simple:

A lot of small teams do not have a dedicated AppSec engineer.
Security reviews often happen too late.
Reports from bigger tools can be too complex, too noisy, or too expensive for early-stage projects.
And developers usually need something direct:

What is wrong?
Why does it matter?
How can I fix it?

That is the direction I took with PentestScan.

What PentestScan does

PentestScan is a Web & API security scanner focused on practical checks around common security weaknesses.

The current version is built around:

Web application scanning
API security testing
OWASP Top 10 related checks
Security headers analysis
JWT and session-related checks
Basic exposure detection
Report generation
DevSecOps-friendly workflow

The goal is not to overload the user with hundreds of unclear findings.

The goal is to provide a clean report that helps someone understand the risk and take action.

Tech stack

The project is built with a simple and practical stack:

Python
FastAPI
Docker
Nginx
HTML reporting
API-first backend structure
Security-focused scanning modules

I wanted the architecture to stay modular because security tooling can become messy very quickly if everything is placed into one large script.

So the scanner is organized around separate modules and checks, with the idea that new functionality can be added gradually without breaking the existing structure.

Why I built it

I built PentestScan mainly as a hands-on DevSecOps project.

I wanted to combine several areas that I work with and care about:

application security
backend development
automation
Linux deployment
Docker-based services
CI/CD security thinking
security reporting

One thing I learned while building it is that detection is only one part of the problem.

The harder part is explaining the finding in a useful way.

A security report should not only say:

Missing security header detected.

It should explain:

what was detected,
why it matters,
how it could be abused,
how serious it is,
and what the developer can do next.

That became one of the most important ideas behind the project.

What I am trying to improve

PentestScan is still evolving.

Some of the things I am working on or planning to improve are:

better API scanning
cleaner report structure
more contextual findings
CI/CD integration
better severity scoring
improved attack-path style explanations
more useful remediation guidance
public sample reports

I am also trying to keep the tool realistic.

Security scanners can easily create false confidence.
Just because a scanner does not find something does not mean the application is secure.

So I see PentestScan as a first security layer – something that can help teams catch obvious and common issues earlier, not as a full replacement for manual testing.

Current version

The project is currently available for free here:

https://pentestscan.app/

Before trying the scanner, you can also check a public sample report:

Sample vulnerability report:

https://pentestscan.app/api/v1/public/marketing/vulnerability_report.html

I am sharing PentestScan publicly to get feedback from developers, DevOps engineers, and security people.

I would especially appreciate feedback on:

  • report structure and clarity,
  • usefulness of the findings,
  • scanning flow,
  • missing checks,
  • and whether the tool feels useful for small teams or solo developers.

This project is still a work in progress, but it has already been a valuable learning experience in building security tooling that is not only technical but also understandable and useful.

That, for me, is the main point of PentestScan:

help developers see security issues earlier, understand them faster, and fix them with less friction.

Blazingly Fast Ecommerce Stack for Less Than a Coffee a Month — No Marketplace, No Platform Cut

If you’ve ever looked at a marketplace’s fee page and felt your eye twitch, this post is for you.

The major selling platforms take their cut from every angle — transaction fees, listing fees, monthly subscriptions, payment processing. The percentages vary but the direction doesn’t: a meaningful slice of every sale goes to infrastructure you don’t own or control. And that’s before the visibility problem: on a marketplace of millions of listings, the algorithm decides whether your work gets seen at all.

My girlfriend had been listing her work on one of the big marketplaces for a while — barely any traffic, zero sales. The fees were almost beside the point. I saw the disappointment and floated the idea: her own site, her own corner of the internet — and I’d build it.

I’m a software engineer. I like a challenge. So I set out to give it a shot. The result runs at €3.29/mo for all backend infrastructure. The site is live at nadiapoe.co.uk. Here’s the stack and the decisions behind it.

The constraints

  • Artwork loads instantly. Watercolours are the product. A 2-second LCP would send visitors away before they saw a painting. No image-CDN that stamps a watermark on the hero image, no spinner while the page wakes a cold serverless function.

  • No third-party watermarks. Image-CDN convenience — Cloudinary, imgix — isn’t worth a logo in the corner of the hero. This is an artist’s portfolio. The paintings deserve respect.

  • Real commerce, not a payment link. Multi-currency (GBP / EUR / USD / AUD), international fulfillment for prints, originals shipped from the UK. A Stripe payment link in the Instagram bio wasn’t going to cut it.

  • No tracking, no cookie banner. The site doesn’t follow visitors. No analytics cookies, no third-party pixels, no consent popup to dismiss before you can see a painting. Purchase data goes only as far as it needs to: card details to Stripe, a shipping address — originals are shipped by us from the UK, prints via a print-on-demand provider for international orders. Nothing retained beyond what’s needed to get the order out the door.

  • Indie budget. One artist, no team, no investor. Anything more than ~£10/month total infrastructure is a recurring tax on the creative work. That ceiling shaped every hosting decision in the stack.

The stack, piece by piece

The infrastructure splits cleanly across two providers.

Cloudflare (free tier)

Everything the visitor touches runs on Cloudflare. CDN and WAF sit in front of everything — Bot Fight Mode, rate limiting, R2 hotlink protection. Behind them:

Cloudflare Pages hosts the Astro 6 storefront. Static by default, per-route SSR where needed (checkout callbacks, contact form, structured data feeds). The painting pages are pure HTML — the entire collection ships zero JS until the cart is opened.

Cloudflare R2 stores all images and process videos. Zero egress fees, served from a custom domain (media.nadiapoe.co.uk). Videos are pre-encoded to four variants locally with ffmpeg (720p desktop, 480p mobile, JPEG poster, 64×64 thumb) and uploaded via wrangler. No URL transforms, no image-service quotas to exhaust.

Cloudflare D1 is the edge SQL database — one table, one purpose: like counts. The like button stores your choice in localStorage and increments a counter in D1. No cookies, no tracking, no consent popup. You see the count; nothing sees you.

Hetzner VPS — €3.29/mo

Everything commerce-related runs on a single Hetzner CAX11: 2 vCPU ARM64, 4 GB RAM, 40 GB NVMe, 20 TB/month egress. On it:

  • Medusa v2 — the commerce backend. Dev and prod as separate systemd services, both reverse-proxied by Caddy.
  • PostgreSQL — orders, products, customers.
  • Redis — BullMQ event bus and workflow engine.

Render, AWS, and Oracle Always Free each failed for a different specific reason. Hetzner just works.

One gap in Medusa v2 worth knowing: there’s no official Resend provider. Order confirmation emails skip the notification module entirely and call the Resend SDK directly from an order.placed subscriber.

Third-party services

Stripe handles payments across four currencies (GBP / EUR / USD / AUD). Cloudflare injects cf.country into a <meta> tag at the edge; the storefront reads it to pick the matching Medusa region and present prices in the local currency. User override persists in localStorage.

Resend handles transactional email — 3,000 emails/month free, then $1/1,000.

The interactive layer

Svelte islands handle the cart drawer, quantity controls, painting gallery, and region selector. Nano-stores (cartStore, cartUpdating, regionStore) keep islands in sync without a framework router.

The shop grid itself is static Astro HTML — no Svelte involved. The paintings are the product; a visitor should see the full collection immediately, not wait on an inventory check before anything renders. So every card defaults to “available,” then a plain <script> tag calls the Medusa API in the background and patches data-status on each card to flip sold originals to a faded state. No layout shift, no spinner, no framework overhead for a read-only grid.

One Astro 6 gotcha that cost an afternoon: server-side secrets must come from import { env } from 'cloudflare:workers'. The old Astro.locals.runtime.env was removed and import.meta.env silently bakes undefined for server-only vars. No build error, no runtime warning — just missing data in production.

What it actually costs

Backend hosting: €3.29/mo — both staging and production Medusa instances, PostgreSQL, Redis, Caddy, automated nightly backups to Cloudflare R2. Frontend, CDN, edge SQL, object storage, WAF, analytics: free tier. The metered costs are Stripe (1.5–2.9% per transaction — unavoidable regardless of platform, but at least there’s no extra platform cut on top) and Resend (3,000 emails/month free, then $1/1,000).

Compare that to a typical marketplace taking 6–7% on a £150 original watercolour — that’s £9–10 per sale, forever, to infrastructure you don’t own. At even modest volume the self-hosted setup pays for itself inside the first month.

Will this survive a traffic spike? Honestly, no idea — this shop has never been Slashdotted, and a 2 vCPU ARM box with 4 GB RAM is not going to win any load test. But if it ever buckles under the weight of people trying to buy original watercolours, upgrading the VPS will be the easiest problem on the list that day.

One snippet worth stealing

This pattern only works because the HTML is served through a Cloudflare Worker — but if you’re already on Cloudflare Pages, you have that for free.

The full CSP is set per-request with a fresh nonce, injected into every <script> tag at the edge. No build-time hash dance, no list of inline script hashes to maintain. From client/src/middleware.ts:

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next()
  if (typeof HTMLRewriter === 'undefined') return response
  if (!response.headers.get('content-type')?.includes('text/html')) return response

  const nonceBytes = crypto.getRandomValues(new Uint8Array(16))
  const nonce = btoa(String.fromCharCode(...nonceBytes))

  const rewritten = new HTMLRewriter()
    .on('script', { element(el) { el.setAttribute('nonce', nonce) } })
    .transform(response)

  const headers = new Headers(rewritten.headers)
  headers.set('Content-Security-Policy', buildCsp(nonce))
  return new Response(rewritten.body, { status: rewritten.status, headers })
})

Cloudflare’s own bot-fight challenge scripts get whitelisted automatically — they read the nonce from the CSP header and stamp themselves with it. On a static-only setup you’re stuck with hashes, and those break the moment Cloudflare rotates a challenge token. The Worker approach sidesteps that entirely.

The site is nadiapoe.co.uk if you want to see the result.

Navigation API Reaches Baseline Newly Available as Replacement to the History API

The Navigation API is a new interface for managing client-side navigation in single-page applications, now available in major browsers as of January 2026. It addresses limitations of the prior History API by providing a unified event model and improved history management. Key features include the navigate event, automatic URL updates, and integrated error handling.

By Daniel Curtis

TELKODER: “Türk operatörler, gereksiz şekilde uzayan trafik rotaları nedeniyle Avrupa’daki altyapı sağlayıcılarına ek maliyet ödüyor.”

TELKODER:

TELKODER: "Türk operatörler, gereksiz şekilde uzayan trafik rotaları nedeniyle Avrupa’daki altyapı sağlayıcılarına ek maliyet ödüyor."“Bilgi Teknolojileri ve İletişim Kurumu (BTK), 5 Mayıs’ta işletmecilere ilettiği yazıyla, hedefi Türkiye olan internet trafiğinin yurt dışına çıkmadan iletilmesine yönelik tedbirlerin hayata geçirilmesini gündeme taşıdı. Türkiye’nin verisinin mümkün olduğunca ülke sınırları içinde dolaşması; milli güvenlikten hizmet kalitesine, gecikme sürelerinden maliyet optimizasyonuna kadar birçok açıdan stratejik önem taşıyor. Ancak bu hedefe ulaşmak için yalnızca çağrı yapmak yeterli olmuyor. Sorunun kaynağını doğru tanımlayan, yapısal eksiklikleri gideren ve tüm paydaşları kapsayan bütüncül bir yaklaşım gerekiyor.” diyen TELKODER (Serbest Telekomünikasyon İşletmecileri Derneği), bugün paylaştığı açıklamada, “Türkiye’den komşu ülkelere iletilen bir e-posta, görüntülü görüşme ya da basit bir veri paketi çoğu zaman beklenmedik rotalar izliyor. Trafik çoğunlukla Türkiye’den çıkarak İtalya, Almanya ve İsveç gibi düğüm noktalarından geçiyor, ardından hedef ülkeye ulaşıyor. Örneğin İstanbul’dan Bakü’ye gönderilen verinin önemli bir bölümü Stockholm üzerinden yönleniyor. Benzer şekilde Türkiye’den Gürcistan’a giden veri paketlerinin önemli bir kısmı ülke sınırları içinde kalmadan yoluna devam ediyor. Bu yapı hem ekonomik hem […]

TELKODER: “Türk operatörler, gereksiz şekilde uzayan trafik rotaları nedeniyle Avrupa’daki altyapı sağlayıcılarına ek maliyet ödüyor.” yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.