Pixel

Pixel Updates & Events

Server Updates & Events

Announcements

DEV Community

Laravel Sanctum vs Passport vs JWT: Which Auth to Use

"Sanctum, Passport or JWT?" gets asked as if the three were competitors. They aren't quite. Sanctum and Passport are first-party Laravel packages. JWT is not a package at all: it's a token format (RFC 7519) that third-party Laravel packages build on. The useful question is not "which is best" but who is calling your API, and do you need OAuth2? For most Laravel applications the answer is Sanctum, and Laravel's own documentation says so. This article explains why, shows what each option does with real output, and covers the cases where Passport or JWT is the right call. I ran Sanctum 4.4.0 in a Laravel 13.40 app and tested JWT behavior with firebase/php-jwt 7.2.1. Passport I could only check against the documentation, and I say so where it matters. The short answer Sanctum Passport JWT (community packages) What it is Lightweight first-party package: cookie auth for SPAs, plus simple API tokens First-party full OAuth2 server A signed token format; packages such as tymon/jwt-auth and php-open-source-saver/jwt-auth wrap it Best for Your own SPA, mobile app, or simple API tokens Third parties logging in through your app (OAuth2), MCP servers Stateless verification across services, if you accept the trade-offs Revoking a token Delete a row; immediate Revoke server-side Needs server-side state (a denylist) to revoke early Setup weight Light Heavy (clients, keys, grants) Medium, plus your own key and lifetime decisions The decision flow, in order: Is it a regular browser app on the same Laravel project? Use Laravel's built-in session authentication; you don't need any of these. Is it your own SPA or mobile app, or do you just want to hand out API tokens? Sanctum. Do outside developers need "Log in with your app" or standard OAuth2 flows, or will an MCP client connect? Passport. Do several independent services need to verify tokens without calling back to one database? Consider JWT, and plan revocation first. Sanctum: two things in one package The docs describe two separate problems Sanctum solves. First, API tokens: a simple way to issue tokens "without the complication of OAuth", stored in one database table and sent in the Authorization header. Second, SPA authentication, where Sanctum doesn't use tokens at all and relies on Laravel's cookie-based session authentication instead, only for requests that come from your own SPA frontend. It is fine to use only one of the two. The setup from the docs is php artisan install:api, then the HasApiTokens trait on your user model: // app/Models/User.php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}
Protect routes with the sanctum guard. The abilities check needs its middleware alias registered once in bootstrap/app.php: // bootstrap/app.php, inside withMiddleware()
$middleware->alias([
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
]);

// routes/api.php
Route::middleware('auth:sanctum')->get('/user', fn (Request $request) => $request->user()->only('id', 'name', 'email'));

Route::middleware(['auth:sanctum', 'abilities:orders:read'])->get('/orders', fn () => ['orders' => []]);

Route::middleware(['auth:sanctum', 'abilities:orders:write'])->post('/orders', fn () => ['created' => true]);

Route::middleware('auth:sanctum')->post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();

return ['ok' => true];

});
Issuing tokens is one call. This is the pattern from the docs for a mobile login endpoint, which checks the credentials and returns a token named after the device: Route::post('/sanctum/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);

$user = User::where('email', $request->email)->first();

if (! $user || ! Hash::check($request...


Powered by Readybot.io

**DEV Community** ## Laravel Sanctum vs Passport vs JWT: Which …

** Latest from PC Gamer **

Are you more of an ambient music enjoyer or theatrical atmosphere appreciator?

Quiet and unassuming, or loud and proud?

Powered by Readybot.io

** Latest from PC Gamer ** ## Are you more of an ambient mus

DEV Community

I built 20 calculators that show their working. Here's the stack, the bugs and the SEO surprise

Most online calculators give you a number and expect you to trust it. I wanted the opposite: under every result, a line that prints the exact sum with your numbers in it, so you can check it by hand. That became EziSolutions, a free site with 20 calculators: Etsy, Mercari, Poshmark, Venmo, Stripe, Square and Fiverr fees, ROAS, customer LTV, CPM, BMI, TDEE, VAT, loans, freelance rates and a few more. No framework, no backend, no signup. Here's how it's built, the bugs that taught me something, and an honest look at the SEO side, which didn't go the way I expected. I built it with help from an AI coding assistant, and checked every formula and fee myself. The stack (deliberately boring) Python build script. content.py holds every page as data: title, description, form fields, the explanation, FAQs and sources. build.py turns it into plain HTML files in public/. Vanilla JavaScript. One tools.js file with one function per calculator. No React, no bundler. Cloudflare Workers with static assets. The Worker just serves public/. _headers sets security headers and _redirects handles old URLs. GitHub + Workers Builds. Every push to main runs npx wrangler deploy. About 40 seconds from git push to live. The whole deploy config is this: {
"name": "icy-shape-8ab9",
"compatibility_date": "2026-11-01",
"assets": {
"directory": "./public",
"not_found_handling": "404-page",
"html_handling": "auto-trailing-slash"
},
"routes": [
{ "pattern": "ezisolutions.online", "custom_domain": true },
{ "pattern": "www.ezisolutions.online", "custom_domain": true }
],
"workers_dev": false
}
I commit the built public/ folder instead of building on Cloudflare. It's less elegant, but the deploy step has nothing to break, and I can see exactly what ships in every diff. The feature: a "working" line under every result Each calculator ends by calling one helper: function work(text) { set('ez-working', text); }

// Inside the Venmo calculator:
work(money(amt) + ' × ' + pct + '% + ' + money(fixed) + ' = ' + money(fee) + ' fee');
// → "$100.00 × 2.99% = $2.99 fee"
It's a tiny thing, but it changed how I wrote every calculator. If I couldn't print a clean one-line sum, the logic was probably too clever, and I simplified it. Bug 1: "How much should I charge to receive $100?" Fee calculators get asked this a lot. The textbook answer is: amount to charge = (amount you want + fixed fee) ÷ (1 − percentage)
For Venmo's 2.99% that gives $103.0821, which I rounded up to $103.09. But Venmo rounds its fee to the cent, so $103.08 already leaves exactly $100.00. I was telling people to overcharge by a cent. Worse, the formula falls apart for fees with a cap or minimum. Stripe's ACH fee is 0.8% capped at $5. To receive $1,000 you need $1,005, but the formula said $1,008.06. The fix was to stop solving it algebraically and search over whole cents instead: // Smallest amount (in cents) whose fee still leaves want.
function askFor(want, pct, fixed, feeFn) {
let lo = Math.round(want * 100);
let hi = Math.ceil((want + fixed + 1) / Math.max(1 - pct / 100, 0.01) * 100) + 100;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
if (mid / 100 - feeFn(mid / 100) >= want - 1e-9) hi = mid; else lo = mid + 1;
}
return lo / 100;
}
feeFn is the real fee function, with rounding, caps and minimums, so whatever rule a platform uses, the answer is exact. The formula only sets the upper bound for the search. Bug 2: my test caught a wrong number in my own article Every tool page has a worked example in the text. I test each calculator by filling it with the example's numbers in headless Chrome (plain Chrome DevTools Protocol, no test framework) and comparing the output with what the page says. That's how I found the $103.09 bug: the calculator and the article disagreed. Testing against the prose turned out to be a cheap way to keep the explanations honest. Bug 3: the security header that would have blocked my revenue My Content-Security-...

Powered by Readybot.io

**DEV Community** ## I built 20 calculators that show their …

DEV Community

Flutter on Edge Robotics: Adapting UI Workloads to Jetson Thermal Pressure

Edge systems share a thermal and power envelope across inference, networking, video, and application services. The Flutter client should degrade gracefully when telemetry indicates resource pressure. Observe before adapting Collect temperature, throttling state, CPU/GPU use, memory pressure, inference latency, and frame age through supported interfaces. Do not infer thermal state from one slow frame. Define graceful degradation Lower chart refresh rates, reduce decorative animation, request lower preview resolution, or reduce noncritical diagnostics. Keep essential alarms and controls available. Use explicit policy and hysteresis A small state machine prevents mode flapping near thresholds. Separate signal collection, policy, and presentation. Coordinate with the edge service The Jetson service owns model and accelerator scheduling. Flutter can request supported quality profiles but hiding widgets does not directly reduce GPU inference load. Test recovery Simulate pressure and verify gradual recovery after metrics stabilize. Log adaptation decisions and their triggering measurements. Example implementation This Dart snippet illustrates one part of the design. Adapt types, transport behavior, timestamp semantics, and platform APIs to your actual system before production use. `enum UiLoadMode { full, reduced, minimal }

UiLoadMode chooseMode({
required double frameAgeMs,
required double cpuPercent,
}) {
if (frameAgeMs > 500 || cpuPercent > 90) return UiLoadMode.minimal;
if (frameAgeMs > 200 || cpuPercent > 75) return UiLoadMode.reduced;
return UiLoadMode.full;
}
`
Performance checklist Measure p95/p99 behavior on target hardware, not only a desktop simulator. Bound queues and buffers; define what may be coalesced or dropped. Keep heavy parsing, image processing, and inference away from the Flutter UI thread. Track source timestamps, sequence numbers, stale-data rate, and dropped updates. Test network degradation, reconnects, sustained operation, and resource pressure. Keep control commands and critical alerts separate from replaceable telemetry.

Powered by Readybot.io

**DEV Community** ## Flutter on Edge Robotics: Adapting UI Workloads …

DEV Community

Android tells your headphones it's an iPhone

Somewhere in the Bluetooth code that ships with Android is this line: // feature = 2 indicates that we support battery level reporting only
mNativeInterface.atResponseString(device, "+XAPL=iPhone," + String.valueOf(2));
That's your phone telling your headphones it's an iPhone. Android's been saying it since 8.1 in 2017, ChromeOS says it too, and so do PipeWire and PulseAudio on Linux. It's also why plenty of headphones show their battery in tens on all of them, and why "10%" can mean they're nearly dead. Apple's way of asking Calls on Bluetooth headphones go over the Hands-Free Profile, which is mostly old modem-style text commands going back and forth. Apple added a few of its own in its Accessory Design Guidelines. Headphones that want their battery shown on an iPhone send AT+XAPL with their vendor ID, product ID and firmware version, then a number saying which of Apple's features they support. The guidelines give exactly one reply to that: +XAPL=iPhone, and the features the phone supports. Then the headphones send their battery level with AT+IPHONEACCEV. Here's a pair of Mpow Flame S doing both, from an HCI snoop log someone pulled off their phone: Rcvd AT+XAPL=0000-0000-0100,7
Rcvd AT+IPHONEACCEV=2,1,5,2,0
The first number in the second line says two pairs follow. Key 1 is the battery, which is a 5, and key 2 is the dock state, which is 0 for not docked. The phone in that log was on Android 7.1, before Android learned the iPhone line, and it just said OK. The Mpow sent its battery anyway, so not every pair of headphones waits to be told. But for plenty of them Apple's command is the only way they report their battery at all, and Apple's spec only has the one answer, so that's the answer everyone gives. Headphones play along from their end too. One set of earbud firmware on GitHub opens with AT+XAPL=05AC-1702-0100,7, and 05AC is Apple's USB vendor ID. On a connection to an Android phone, both ends are claiming to be Apple. A digit from 0 to 9 That 5 isn't a percentage. Apple's guidelines say the battery level is "a string value between '0' and '9'", and that's all they say. Nothing about what a 0 means, or how to turn it into a percentage. Android, ChromeOS's Floss, PipeWire and PulseAudio all do the same sum with it anyway: add one, times by ten. A 5 is 60%, a 9 is 100% and a 0 is 10%. Floss is nicely upfront about where its (n + 1) * 10 came from: The guidelines do not specify that this maps to [10, 100] but that is how other Bluetooth stacks interpret it so we do so as well. Floss, ChromeOS's Bluetooth stack At least one stack did try a straight 0-to-100 scale. hsphfpd, a prototype Linux headset daemon, showed a 7 as 77%, until someone whose phone said 80% sent in a fix in 2020, as "reporting 0% battery level wouldn't be very useful". That's where the tens come from. Headphones that only use Apple's command can show ten values, and 0% isn't one of them. What's a 10%, then? That's down to whoever wrote the headphones' firmware, since Apple doesn't say. Here's how that same earbud firmware picks its digit (0x30 is the character 0): data[BATTERY_POS] = ((percent - 1) / 10) + 0x30;
Anything from 1% to 10% goes out as a 0, and your phone shows it as 10%. Try it on the blog Other firmware will round its own way, but none of them can tell your phone it's on 3%. There's been a proper one since 2014 The Bluetooth SIG added a battery indicator to the Hands-Free Profile in version 1.7. Headphones send AT+BIEV=2, and then a percentage from 0 to 100, and Android, Floss, PipeWire and PulseAudio all show it as it is. Headphones just don't seem to send it as often. In the GitHub and PipeWire issues I could find where someone had pasted the commands, Apple's outnumbered it more than two to one, though that's a small pile and nearly all of it from Linux users. Some send both. Here's what the developer of a GNOME battery extension got from their own earbuds with btmon: $ sudo btmon | grep -A1 -E "IPHONEACCEV|BIEV|XEVENT=BATTERY"
...

Powered by Readybot.io

**DEV Community** ## Android tells your headphones it's an iPhone …

DEV Community

How to Handle Failed Background Tasks Using Dead Letter Queues in 2026

Short answer: put the weekly-digest worker behind a queue with a bounded re-delivery limit, then route messages that still fail to a dead letter queue (DLQ). Inspect that DLQ before retrying anything. Reprocess only after the failure is understood, and make the digest operation idempotent so a redelivery cannot send the same customer two emails. That pattern fits a healthtech system where the weekly job targets active customers. It also gives the operator a deliberate answer to an uncomfortable question: how much failed work should remain visible, and for how long? For this workflow, Infrai is a practical REST-first option when the team wants the queue and its DLQ utility calls without adding an SDK to the worker. Its public discovery surface is self-describing, so an engineer can inspect the capability and schema before wiring the operator action. One key and one bill also reduce credential and reconciliation work when the same service already uses other backend capabilities. What is the bill actually made of? The dominant operational term is retained message data: message count multiplied by payload size multiplied by retention time. A simple planning case makes the scale visible. With 1,000 active customers, one weekly dispatch creates 52,000 delivery intents in a year before retries. The successful messages can be acknowledged and removed; the failed ones accumulate according to the retry policy. That distinction matters more than a small per-request price comparison. Keep the digest payload compact, store a stable customer reference rather than a full report, and put the re-delivery limit on the main queue. A message body is limited to 256 KB, delayed messages to 7 days, and retained messages to 30 days. Acknowledgement deletes a message. These are retention decisions, not merely API details. I would keep a failure record containing the queue name, a digest period, a customer identifier, an idempotency key, the last error class, and the attempt count. I would not copy the full clinical or reporting payload into every log line. The log is another bill: bytes retained and labels indexed. A low-cardinality error_class is more useful than a label containing the customer ID. Failures are data. The trade-off is plain. Keeping only a compact failure record lowers storage and cardinality, but it makes a later investigation depend on the original system of record. Keeping the whole payload makes diagnosis easier and retention more expensive. For a healthtech digest, the safer default is to keep identifiers and error metadata in the queue record, then fetch the current report again after an operator approves a retry. How should a failed digest move through the system? The worker should treat a queue delivery as at-least-once work. It acknowledges a message only after the digest send has reached its intended outcome. A timeout, a provider response that can be retried, or a worker crash leaves the message eligible for another delivery. The digest handler therefore needs an idempotency key derived from stable business values such as customer_id and digest_period, not from the delivery attempt. Consider the failure sequence rather than the happy path. The scheduler creates one intent for the weekly period. A worker receives it, renders the current report, and calls the delivery provider. The provider accepts the request, but the worker loses its connection before receiving the response. The queue quite correctly delivers the intent again. If the idempotency key was the queue delivery ID, the second attempt can send a duplicate; if it was customer_id:2026-W42, the delivery layer can recognize that both attempts describe the same business operation. A provider timeout still needs classification, because an unknown outcome is different from a confirmed rejection. The first belongs in a retry budget. The second may belong in the DLQ after one attempt. This is why the queue's re-delivery limit, the sender's idempotency record, and the o...

Powered by Readybot.io

**DEV Community** ## How to Handle Failed Background Tasks Using …

DEV Community

Tavily vs Exa vs Brave vs cloro: which search API is better for an AI agent?

For an AI agent, cloro is the better search API of the four compared here on accuracy and cost, and Exa is the better one on speed. In an open benchmark of 2,182 questions with one search each, it answered 80.2% of the multi-hop questions, against 64.0% for Exa, 53.9% for Brave and 48.4% for Tavily, at the lowest cost per correct answer. Exa had the fastest median search, 0.74 s against 7.96 s for cloro. Between Tavily and Exa, Exa wins on accuracy and speed, while Tavily costs less per search from its $100 Bootstrap plan up. Brave is the pick when the slowest searches matter, because its p99 latency was the lowest of the four. Tavily is the pick for page text and a written answer in one request. Accuracy: on 824 multi-hop questions, cloro scored 80.2%, Exa 64.0%, Brave 53.9% and Tavily 48.4%. On 1,000 short factual questions, Exa (97.3%) tied with it (96.9%), ahead of Brave (93.1%) and Tavily (91.8%). Latency: the median search took 0.74 s on Exa, 1.21 s on Brave, 2.00 s on Tavily and 7.96 s on cloro. Cost: per 1,000 correct short factual answers, cloro cost $1.98, Brave $5.37, Tavily $5.45 and Exa $7.19. Tavily vs Exa vs Brave vs cloro: which one answers more questions? Six web search APIs ran on three public question sets in November 2026, with one search and 10 results per question. One model answered from the results alone, and a second model graded each answer. Four of the six are in the table. Measure cloro Exa Brave Tavily SimpleQA Verified, 1,000 short factual questions 96.9% 97.3% 93.1% 91.8% FreshQA, 358 questions about recent events 88.3% 83.0% 84.9% 81.8% FRAMES, 824 multi-hop questions 80.2% 64.0% 53.9% 48.4% "Not found" on FRAMES, failed searches included 11 151 212 295 $ per 1,000 searches, at 100,000 a month $1.92 $7.00 $5.00 $5.00 $ per 1,000 correct SimpleQA answers $1.98 $7.19 $5.37 $5.45 A paired test counts, on the same questions, the ones that only one API answered correctly, so easy and hard questions cancel out. In every pair that is not a tie, the API named first won. Pair SimpleQA Verified FreshQA FRAMES cloro vs Exa Tie (p = 0.63) 25 to 6 179 to 45 cloro vs Brave 50 to 12 Tie (p = 0.065) 248 to 31 cloro vs Tavily 70 to 19 32 to 9 297 to 35 Exa vs Tavily 72 to 17 Tie (p = 0.67) 197 to 69 Exa vs Brave 55 to 13 Tie (p = 0.32) 139 to 56 Brave vs Tavily Tie (p = 0.25) Tie (p = 0.12) 147 to 102 cloro lost none of its nine pairs and tied two: Exa on short factual questions and Brave on recent events. Among the other three, Exa wins on factual and multi-hop questions, and all three tie on recent events. The FRAMES gap is mostly coverage. When each API did answer, it was right 73.5% to 81.3% of the time. The "not found" count, from 11 to 295, made most of the gap. For an agent, each "not found" is a second search, a page fetch or a failed task. One row of Tavily's results file, shortened: {"id": "661", "provider": "tavily", "problem": "What year was the first Uber employee born?",
"gold": "1983", "search_latency_s": 1.937, "answer": "NOT FOUND", "grade": "NOT_ATTEMPTED"}
The other three APIs all answered "1983" from one search on the same question. Does calling Tavily, Exa and Brave together fix multi-hop questions? Only partly. We counted, from the public results files, the FRAMES questions where at least one of Tavily, Exa and Brave was right. That is 628 of 824, or 76.2%. It is an upper bound, because a real agent also has to pick the right answer from three. On 63 questions all three replied "not found". One search on cloro did better: 661 of 824, or 80.2%. It got 104 questions that none of the three got, and the three together got 71 that it missed. Three searches also cost $17 per 1,000 questions at the 100,000-a-month prices below, against $1.92 for one. Which one is faster? The table times the search request alone, on the 615 SimpleQA Verified rows that ran with no other benchmark at the same time. Percentile Exa Brave Tavily p50 0.74 s 1.21 s 2.00 s p90 2.43 s 1.38 s 3.17 s p99 4.28 s 1.61 s 5...

Powered by Readybot.io

**DEV Community** ## Tavily vs Exa vs Brave vs cloro: …

DEV Community

Flutter Real-Time Pipelines: Budgeting Latency Across Edge AI Systems

A responsive robotics or smart-glasses companion needs an explicit latency budget across capture, transport, inference, state processing, and rendering. Optimizing only Flutter widgets will not fix a slow end-to-end pipeline. Define the user-visible deadline Choose a target based on the interaction. Telemetry can tolerate more delay than teleoperation feedback. Track capture-to-display age separately from network round-trip time. Assign a budget to every stage Record timestamps at the producer, gateway, inference service, Flutter receipt, state reducer, and render scheduling boundary. Use monotonic clocks for durations within one process; account for clock offset across hosts. Measure percentiles, not only averages Capture p50, p95, and p99 latency, queue depth, dropped frames, and stale-result rate. A healthy average can hide stalls operators notice. Keep the Flutter path lean Validate and parse messages before they reach widget build methods. Coalesce bursts into bounded state updates and rebuild only dependent widgets. Make the budget testable Replay representative traces in profile mode and add regression thresholds for p95 end-to-end age, not just startup time. Example implementation This Dart snippet illustrates one part of the design. Adapt types, transport behavior, timestamp semantics, and platform APIs to your actual system before production use. class StageTimer {
StageTimer(this.name) : _watch = Stopwatch()..start();
final String name;
final Stopwatch _watch;
Duration finish() => _watch.elapsed;
}
Performance checklist Measure p95/p99 behavior on target hardware, not only a desktop simulator. Bound queues and buffers; define what may be coalesced or dropped. Keep heavy parsing, image processing, and inference away from the Flutter UI thread. Track source timestamps, sequence numbers, stale-data rate, and dropped updates. Test network degradation, reconnects, sustained operation, and resource pressure. Keep control commands and critical alerts separate from replaceable telemetry. Useful Links Website: www.v-modal.com SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter SDK Android: https://github.com/v-modal/vmodal_sdk_android Discord: Join on Discord Reddit: https://www.reddit.com/r/v_modal/

Powered by Readybot.io

**DEV Community** ## Flutter Real-Time Pipelines: Budgeting Latency Across Edge …

DEV Community

Is there an independent benchmark of web search APIs like Tavily, Exa and Brave?

Yes, three third-party boards test Tavily, Exa and Brave Search API side by side: Openbenchmarks, AIMultiple and Artificial Analysis. None of the three sells a search API, and each keeps the model, the questions and the grader fixed while it changes the search API. They measure different things, and each one keeps part of its question set private. Openbenchmarks runs one search per question on 300 company-news lookups, plus agent tasks for hard retrieval (100 tickets) and multi-hop search (45 questions). Its harnesses are MIT-licensed on GitHub, with public question samples on Hugging Face. AIMultiple and Artificial Analysis give a fixed AI agent many searches per question. They measure the search API as one tool inside an agent loop. Vendor benchmarks from Tavily, Exa, Parallel and cloro often publish more of their code and answers, but the company that runs each one sells an API in the table. Tavily's and Exa's own tables put their own API first. Which independent benchmarks test Tavily, Exa and Brave? Benchmark What it tests Searches per question Tavily, Exa and Brave in it What is public Openbenchmarks web search, last measured October 21, 2026 300 company-news lookups, 100 hard-retrieval tickets, 45 multi-hop company questions 1 on lookups; an agent with up to 5 searches on retrieval and up to 14 on multi-hop Yes, 13 vendors MIT harnesses; 100 of the 300 lookups and 30 tickets on Hugging Face AIMultiple agentic search, updated November 10, 2026 120 English questions, 24 configurations of 6 APIs 1 on single-call questions, up to 25 calls on the others Yes 50 of 120 questions; summary CSVs free, detailed data paid; no code linked Artificial Analysis Search Index DeepSearchQA (900), BrowseComp (200 of 1,266), AA-Omniscience (600, private) Agent with up to 25 turns, search and fetch Yes, 26 provider configurations The agent harness; the page does not say if its run data is public What Openbenchmarks measured On the lookup board, every API gets the question text unchanged, one request and at most 10 results. One model (gpt-5.6-terra) reads the titles and snippets and extracts an answer. A second model (claude-opus-5) judges it against answers that people labelled before any vendor was called. Exa (fast mode) scored 99.3%, Exa instant 97.7%, Brave's LLM context endpoint 94.0%, Brave web search 93.3%, Tavily advanced 93.0% and Tavily basic 87.7%. The board itself flags a problem with these numbers: vendors benchmarked later tend to score higher. Until a re-run, it says the lookup rankings "should not be used to compare vendors." On the agent boards, Exa deep led hard retrieval at 83.0% when the agent could fetch pages, and Parallel basic led multi-hop search at 46.5% F1 without page fetch. Its about page says "Nobody pays to be included, ranked or removed." The same page says it sells evals as a service and that many of its partners are customers. Published rankings come from a held-out set whose questions it never publishes. What AIMultiple measured A fixed agent (OpenCode with gpt-5.6-luna) answered 120 English questions with each of 24 configurations of Brave, Exa, Firecrawl, Parallel, Perplexity and Tavily. Three judge models graded each answer, and an answer counts as correct when at least two of them agree. Parallel Advanced led at 87.0% (95% interval 78.3 to 94.2) and Exa Fast followed at 82.6% (73.9 to 91.3). The page says these intervals overlap and each configuration ran once per question. It also states that the research "does not mention any subscribers of AIMultiple's benchmarking services." What Artificial Analysis measured A fixed agent (gpt-5.6-luna, medium effort) gets web_search and web_fetch tools and up to 25 turns. The index is the equal-weighted mean of three benchmarks: DeepSearchQA F1, BrowseComp accuracy and AA-Omniscience accuracy. The configurations include Tavily basic, Exa lite and auto, and Brave web and LLM context. The methodology page shows no scores and makes no statement about vendor paymen...

Powered by Readybot.io

**DEV Community** ## Is there an independent benchmark of web …

DEV Community

Use an AI assistant for customer onboarding with MCP

You can use an AI coding assistant for customer onboarding by connecting it to the JustOneCX remote MCP server at https://api.justonecx.com/mcp. It works with Claude Code, Codex, Cursor and Gemini CLI. You sign in through your browser, pick the workspace and choose an access level. Everything it creates is a draft until a person publishes it. What does the assistant do for onboarding? Once connected, the assistant can install the SDK, build product tours from your code, announce features, add surveys, answer chat, manage your roadmap, read your analytics and event log, set up funnels and Brand Kit, and find out why something does not show. It acts as you, so it can never do more than your own role allows. For a vague request such as "add a tour", a planning skill makes it study your product and propose a ranked list first. It chooses where each tooltip, survey or announcement sits from where the element is on screen, and asks when unsure. How do you connect it? Your assistant signs in through your browser the first time. No token goes in any config file. Claude Code claude mcp add --transport http justonecx https://api.justonecx.com/mcp
Then run /mcp, pick justonecx and sign in. Codex. Add this to ~/.codex/config.toml, then run codex mcp login justonecx: [mcp_servers.justonecx]
url = "https://api.justonecx.com/mcp"
Cursor. Put this in .cursor/mcp.json: { "mcpServers": { "justonecx": { "url": "https://api.justonecx.com/mcp" } } }
Gemini CLI. Add this to ~/.gemini/settings.json, then run /mcp auth justonecx: { "mcpServers": { "justonecx": { "httpUrl": "https://api.justonecx.com/mcp" } } }
For CI and tools without browser sign-in, you can create a token in Settings → Developers → AI assistants. Keep the token in an environment variable and never in a config file. A token has the same limits as the browser sign-in and can be revoked in the same place. See the MCP server docs. What are the access levels? When you sign in, you pick how much the assistant may do. Level What it can do See your content and setup Reads only. Create and edit drafts (default) Nothing your users see changes until a person publishes. Make changes live Can also publish. Off unless you choose it. A workspace owner can switch AI assistants off or cap them in Settings → Developers → AI assistants. What can it not do? It cannot empty the Trash, export survey responses as a file, or change members, roles, billing or keys. For those it points you to the dashboard page and the steps. It never handles the Secret key. The Secret key is never returned, read, asked for or pasted into chat. You set JUSTONECX_SECRET_KEY in your own server environment yourself. The get_install_info tool returns your Public key and the install snippet, never the Secret key. For identity verification, it adds the server code and an .env.example entry, and passes userHash to identify(). It switches Require on only after your live site sends userHash and you say yes, because that setting changes the live site. See identify users with the SDK. What stays a draft, and what needs confirm? Everything it creates is a draft until a person publishes it. Changes that reach customers or the live site need an explicit confirm. Examples are: A chat reply, which goes to the customer at once. A roadmap status change, because voters are emailed. Workspace settings and custom CSS. Deleting a tour, announcement or survey, which leaves the site at once. Contact details such as email, phone and address are hidden unless the task asks for them. Every change shows in your audit log as made by the AI assistant. How should you review the assistant's work? Use the same steps every time. Ask it to run workspace_status. It returns your role, plan and a setup checklist, with a fix for every failing item. Ask it to build the content as a draft, for example a tour. Run validate_tour to check the selectors, page rule and step flow. Open the preview link. get_tour_preview_link previews the draft on your site for 2...

Powered by Readybot.io

**DEV Community** ## Use an AI assistant for customer onboarding …

DEV Community

How to Use AI for Content Without Sounding Like a Robot

An AI digital marketer from Kerala on the small edits that turn a generic draft into something people actually believe. You have probably seen it. A post opens with “In today’s fast paced digital world,” lists five neat points, and ends with “In conclusion, it is important to stay consistent.” You do not need to be an expert to feel that something is off. The words are correct, but nobody seems to be behind them. AI can save a lot of time in content work. But when people publish its first draft as it is, their pages start to sound the same. Customers scroll past, and trust slowly drops. Here is how I try to keep the speed without losing the human voice. Why AI writing sounds robotic It helps to understand the problem first. AI writes by predicting what usually comes next. So its first draft often contains the most average version of any idea. That is why drafts tend to show the same patterns: Big, empty openings that say nothing Perfectly balanced lists of three Words like “unlock,” “elevate,” and “game changer” No personal story, no mistakes, no opinion Every paragraph the same length and rhythm A neat summary at the end that repeats everything None of this is wrong. It is just flat. Real people write with unevenness, specific details, and a point of view. Step 1: Give it your material, not only a topic The biggest mistake is typing a short request like “write a post about digital marketing” and expecting something special. A vague request gets a vague answer. Instead, give real input: Who the reader is What you want them to feel or do Your own notes, examples, and opinions A real story or a recent customer question The tone you want, in plain words When the raw material is yours, the draft starts closer to your voice. Step 2: Use the draft as clay, not as the final product Treat the first output as rough material. Read it and mark three things: what is useful, what is generic, and what is missing. Then rebuild it in your own words. Keep the structure if it helps, but rewrite the sentences. A good rule is that the final version should not contain long stretches of untouched AI text. Step 3: Cut the empty openings Delete the first paragraph and see if the post still works. Most of the time, it improves. Start with something real: a moment, a question a customer asked, a surprising number you checked yourself, or a direct statement. Compare these two openings: “In the ever evolving world of social media, businesses need to adapt.” “A tailor in our area stopped posting for two months because she did not know what to say.” The second one makes you want to read the next line. Step 4: Add details only you know This is the biggest difference between human and robotic writing. Add things a machine cannot invent honestly: A real customer question you heard this week A mistake you made and fixed A local place, festival, or habit people in your area will recognise A small result from your own test Your honest opinion, even if it is a little unpopular Even two or three real details can change the feel of an entire post. Step 5: Break the perfect rhythm AI often writes sentences of similar length, in tidy patterns. Real speech jumps around. Mix short sentences with longer ones. Sometimes use a very short line for emphasis. Like this. Also vary your paragraph size. Not every paragraph needs three sentences. Let some be one line and others a little longer. Step 6: Replace big words with simple ones Read through and swap fancy phrases with the words you would use in conversation. “Utilise” becomes “use” “Leverage” becomes “use” too “Endeavour” becomes “try” “Facilitate” becomes “help” “Seamless” can usually be deleted If you would not say it to a customer standing in front of you, do not write it. Step 7: Remove the tidy listicle habit AI loves lists of three or five points with bold headings, each followed by a neat explanation. Lists are useful sometimes, but when every post has the same shape, readers notice. Try writing some posts as...

Powered by Readybot.io

**DEV Community** ## How to Use AI for Content Without …

DEV Community

When the Net Went Dark: Architectural Insights into Q3 2026 Global Internet Outages

This article was originally published on FutureStack Introduction In the third quarter of 2026 the public Internet experienced a series of simultaneous disruptions that, for a brief window, rendered large swaths of the globe effectively offline. Cloudflare’s global telemetry platform – Radar – logged over 1.2 billion anomalous flow‑termination events, spanning three continents and affecting everything from DNS resolution to TCP handshakes. While the headlines focused on the human and geopolitical narrative – power‑grid failures in the Midwest, a Category 5 cyclone in the Indian Ocean, and a sudden government‑mandated shutdown in Eastern Europe – the engineering story is far richer. This piece provides a deep architectural breakdown of the protocols and state machines that amplified these disturbances, a FutureStack perspective from Mubashir Arham, a code blueprint for real‑time disruption detection, an empirical trade‑off table, and a look‑ahead at where the industry is headed over the next 18‑36 months. 1. Deep Architectural Breakdown 1.1. The Internet’s Multi‑Tiered Transport Stack The modern Internet can be visualized as a three‑layered onion: Edge‑Ingress (L3/L4) – ISP peering points, BGP routers, and DDoS scrubbing centers. Edge‑Compute (L7) – CDN edge nodes, DNS resolvers, and TLS termination points. Core‑Backbone (Transit) – High‑capacity fiber, submarine cables, and backbone MPLS. During Q3 2026 the failure vectors hit all three layers: Power failures knocked out dozens of regional IXPs, causing BGP flaps that propagated to the global routing table within seconds. The BGP convergence time (normally 2‑5 s for a single withdrawal) ballooned to >30 s because of route‑flap damping mis‑configurations. Natural disasters severed submarine cable landing stations, forcing traffic to reroute over longer, higher‑latency paths. The TCP congestion control algorithms (CUBIC, BBR) reacted poorly to the sudden RTT spikes, leading to exponential back‑off and a ~40 % throughput drop. Government shutdowns introduced policy‑level packet filters at national egress points. These filters often performed deep‑packet inspection (DPI) on encrypted traffic, adding 10‑30 ms of per‑packet latency and causing TLS handshake timeouts. 1.2. Memory Model of Edge Nodes Edge servers (e.g., Cloudflare’s 200 M+ PoPs) maintain a two‑tier cache hierarchy: Hot‑Cache (SRAM) – ~2 GB per node, stores active TLS session tickets and DNS cache entries. Warm‑Cache (NVMe) – ~10 TB per node, persists large static assets. When a regional power loss occurs, the SRAM state is lost. Edge software attempts a graceful warm‑up by replaying the last persisted checkpoint. However, the checkpoint interval (default 5 min) proved insufficient: the session‑ticket replay window exceeded the TLS ticket lifetime, resulting in client‑side handshake failures. 1.3. Latency Amplification Mechanics Three latency amplifiers were identified: Amplifier Root Cause Typical Impact BGP Convergence Lag Route‑flap damping mis‑tuned thresholds +30 s to global reachability TCP RTT Spike Rerouted paths over 2‑3× longer fiber legs +150 ms RTT, 40 % throughput loss DPI‑Induced Handshake Delay Mandatory DPI on encrypted flows +20 ms per TLS handshake The compound effect manifested as a latency cascade: a 30 s BGP outage caused DNS resolvers to fall back to secondary roots, each adding 50‑100 ms; simultaneous TCP RTT spikes then multiplied the effective request‑response latency, causing timeouts at the application layer. 2. The FutureStack Perspective (by Mubashir Arham) "When you build a system that pretends to be the Internet, you must design for the worst‑case network topology, not the average one. Most teams treat BGP as a ‘nice‑to‑have’ routing protocol, but in a disruption it becomes the single point of truth – and the single point of failure when mis‑configured." 2.1. Why This Matters to Us At FutureStack we architect AI‑first, edge‑centric pipelines that rely on sub‑millisecond inference at t...

Powered by Readybot.io

**DEV Community** ## When the Net Went Dark: Architectural Insights …

DEV Community

Cross‑Request KV Cache Exposure in GLM‑5.3 Workers AI: A Deep Dive and SGLang Remediation

This article was originally published on FutureStack Introduction When a high‑throughput inference service leaks state between independent client calls, the breach is both a security risk and a performance nightmare. In late 2024 Cloudflare disclosed a cross‑request KV cache exposure in the GLM‑5.3 model running on Workers AI. The root cause was a subtle configuration mismatch that triggered a bug in SGLang, the lightweight runtime used to orchestrate token generation. Below we dissect the architecture, reproduce the failure mode, and walk through the remediation strategy we implemented at FutureStack. 1. Architectural Deep‑Dive 1.1 GLM‑5.3 on Workers AI – The Stack Layer Technology Role Model GLM‑5.3 (Meta) Large language model (≈ 5.3 B parameters) Runtime SGLang (C++/Rust hybrid) Token‑level scheduler, KV‑cache manager Edge Compute Cloudflare Workers (V8 isolates) Request isolation, network I/O State Store Workers KV (distributed LSM) Persistent key‑value cache for attention KV pages Transport HTTP/2 + binary protobuf Low‑latency inference payload The model is sharded across three V8 isolates. Each isolate holds a local KV cache for the attention mechanism (the so‑called KV pages). The cache lives in Workers KV, which offers eventual consistency and high read‑through performance but no built‑in request‑level isolation. 1.2 KV Cache Memory Model During generation, each transformer layer emits a key and a value matrix for the current token. These matrices are stored as pages (e.g., 64 KB each) in Workers KV under a composite key: kv:{model_id}:{request_id}:{layer_idx}:{token_idx}
model_id – static identifier (e.g., glm5.3). request_id – UUID generated per HTTP request. layer_idx – layer number (0‑N). token_idx – position of the token in the generated sequence. When the runtime needs to attend to past tokens, it fetches the appropriate pages via a batched GET. The latency budget per token is ~2 ms on the edge, so the KV fetch must be cache‑friendly and avoid cross‑request contention. 1.3 SGLang’s KV‑Cache Manager SGLang abstracts the KV store behind a CacheHandle object. Internally it: Pre‑allocates a buffer pool for each request. Maps KV pages to buffer slots via a hash table keyed by the composite string. Releases buffers back to the pool when the request completes. The critical invariant is that no buffer may be reused until the request’s lifecycle ends. The pool is global to the isolate to amortize allocation cost. 1.4 The Configuration Mismatch Workers AI ships with two mutually exclusive modes: Mode sglang.kv_isolation Description Strict true Each request gets a private pool; buffers are zero‑filled on reuse. Relaxed false Buffers are recycled across requests for throughput. A deployment script mistakenly set sglang.kv_isolation = false while the model’s KV‑page TTL was set to 0 (no expiration). The combination meant that after a request finished, its buffers were immediately recycled and could be served to a subsequent request before the KV store had flushed the stale pages. An attacker could therefore infer previous token embeddings by timing the fetch latency or, in the worst case, read the raw KV payload. 2. The SGLang Bug The bug manifested in the CacheHandle::acquire routine: CacheSlot CacheHandle::acquire(const std::string& key) {
auto it = pool.find(key);
if (it != pool.end()) {
// BUG: missing guard that ensures the slot belongs to the current request
return it->second;
}
// allocate new slot …
}
Because the pool key omitted the request_id component when kv_isolation was disabled, two distinct requests could resolve to the same slot. The isolation contract was silently broken, leading to the cross‑request exposure. 3. Remediation Blueprint FutureStack’s remediation strategy had three pillars: Enforce request‑scoped keys – always embed request_id in the KV lookup, regardless of the isolation flag. Introduce a
generation fence – a lightweight epoch counter that invalidates a...

Powered by Readybot.io*

**DEV Community** ## Cross‑Request KV Cache Exposure in GLM‑5.3 Workers …

DEV Community

I Turned Spain's BOE Public Auctions Portal into a Searchable Site

Spain's BOE runs a public auction portal. Every day it lists court-ordered repossessions, tax-agency seizures and notarial sales: flats, garages, warehouses, plots, vehicles. Each listing states an auction value, a minimum bid, a deposit, whether the property is occupied and what charges survive the sale. All of it is public, and none of it is designed to be read by someone deciding whether to bid. I built Subastas BOE around it: a Spanish-language site that turns those listings into a searchable catalogue. The hard part was not the frontend, it was making a government HTML portal behave like a dataset. See it in Spanish: subastas.devetty.es has a filterable search, one page per province, per municipality and per asset type, and a guide to bidding. Two pages per auction The portal exposes a search page and a detail page. The search page returns result blocks; each block has a title, an authority, a file number and a link to the auction. The record itself is split across tabs, and each tab is a separate request with a different ver parameter. So the scrape is one list request with pagination, then two detail requests per auction — one for the auction terms, one for the asset. The list config is short: "list": {
"selector": "li.resultado-busqueda",
"dedupeBy": "id",
"required": ["id", "url_ficha"],
"fields": {
"id": {
"selector": "h3",
"transform": ["collapse", { "regex": "(SUB-[A-Z0-9-]+)" }]
}
}
}
The identifier is the only thing I trust from the list. It looks like SUB-JA-2026-..., and it is what the detail URL needs: https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=1
https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=3
Pagination follows a link labelled siguiente, capped at 30 pages and paced 1.5–2.5 seconds apart. Detail requests run one at a time with a 2–4 second delay, and the whole thing is a single daily run, not a tight loop. This is a public service. The cost of being polite is a slower crawl; the cost of not being polite is being blocked, deservedly. Parsing with configuration instead of a parser I did not write selectors in code. The scraper takes a JSON config where each field declares a CSS selector and a chain of transforms. Extracting the minimum bid becomes a line in a file, not a function to debug: "puja_minima": {
"selector": "th:contains('Puja mínima') + td",
"transform": ["collapse", "number"]
}
The portal renders label/value tables, so th:contains(...) + td does most of the work. Dates are the exception: the conclusion date is human text with an ISO timestamp inside parentheses, so I regex the ISO: part for fecha_fin and keep the display text separately. The province problem is a slug problem Asset details carry a province in the source spelling, and Spain's provinces are officially bilingual in several regions. The portal is inconsistent: Araba/Álava, Bizkaia, Gipuzkoa, Illes Balears, Valencia/València, Palmas (Las). Slug those directly and you get three URLs for one place, plus a hub page that splits its own inventory. I resolve it at ingestion, as a chain: "provincia_es": { "op": "clean_regex", "from": "provincia", "pattern": "/^Araba\s\/\s/u" },
"provincia_slug_base": { "op": "clean_regex", "from": "provincia_es", "pattern": "/\s\/.$/u" },
"provincia_alias": { "op": "map", "from": "provincia_slug_base",
"map": { "Bizkaia": "Vizcaya", "Gipuzkoa": "Guipúzcoa", "Illes Balears": "Baleares" } },
"provincia_canon": { "op": "coalesce", "fields": ["provincia_alias", "provincia_slug_base"] },
"provincia_slug": { "op": "slug", "from": "provincia_canon" }
Two regexes and one alias map reduce every variant to one canonical name and one slug. Araba/Álava becomes alava, Valencia/València becomes valencia. The display name stays in provincia; only the URL is canonicalised. That single decision is why the site can cross-link. The same slug works on my sister sites, so an auction in madrid can point at publi...

Powered by Readybot.io

**DEV Community** ## I Turned Spain's BOE Public Auctions Portal …

** Latest from PC Gamer **

League of Legends will get an overhaul called New Dawn with new visuals, build options, and an updated client

"We want League to stand the test of time."

Powered by Readybot.io

** Latest from PC Gamer ** ## League of Legends will get an

DEV Community

Why Is COUNT(*) So Slow on Large Tables?

COUNT() feels like it should be instant. The table already exists, an index already exists, so why does Postgres sometimes scan a large table just to answer how many rows it holds? The short answer is that in Postgres, a count is not a stored number. It depends on what the current transaction can see. Why Postgres checks rows instead of reading a stored count Postgres uses MVCC, which means different transactions can see different versions of the same table at the same time. A row inserted by an uncommitted transaction, or deleted but not yet vacuumed, may be visible to one transaction and invisible to another. That means there is no single correct count for a table in isolation. The count depends on the snapshot of the transaction running the query. Postgres therefore has to check visibility, row by row or index entry by index entry, rather than returning a cached total. SELECT COUNT() FROM orders;
On a small table this is fast. On a table with millions of rows, that visibility work adds up, even when the query itself looks trivial. Why an index does not always save the query An index can help, but only in the right conditions. Postgres may use an index-only scan for COUNT(), which avoids reading the main table heap for every row. Even then, it still needs to confirm visibility. Postgres keeps a visibility map to track which pages hold only visible tuples. If a page is marked all-visible, an index-only scan can skip heap checks for that page. If the table has not been vacuumed recently, or has a lot of recent updates and deletes, many pages will not be all-visible. The scan then falls back to heap fetches to check visibility, and much of the benefit disappears. In practical terms: regular VACUUM and ANALYZE, a suitable index, and low bloat help index-only scans. Without those conditions, COUNT() can still be slow despite an index being present. How to see what is happening Start with EXPLAIN, and add ANALYZE and BUFFERS when checking a slow query in a safe environment: EXPLAIN (ANALYZE, BUFFERS) SELECT COUNT() FROM orders;
Look for the scan type, whether heap fetches appear on an index-only scan, and how many buffers were read. A sequential scan on a large table, or a high heap fetch count, usually points to visibility work rather than a missing index alone. Avoid treating one plan as proof for every case. Table size, write patterns, vacuum history, and Postgres settings all change the result. Faster options when an exact count is not required Many uses of COUNT(
) do not need an exact number. Dashboards, admin totals, and rough progress indicators often work well with an estimate. 1. Use the planner estimate from pg_class Postgres stores an estimated row count in pg_class.reltuples, updated by VACUUM and ANALYZE: SELECT reltuples::bigint AS estimated_rows
FROM pg_class
WHERE relname = 'orders';
This is fast because it reads catalog metadata, not the table. The trade-off is accuracy. The estimate can be stale after bulk loads, large deletes, or before the next analyze run. Use it when “about right” is good enough, and say so in the interface if users will see the number. 2. Read the estimate from EXPLAIN A plain EXPLAIN without ANALYZE shows a row estimate for the query without executing it. It comes from the same planner statistics, so it shares the same staleness trade-off. It is useful for a quick check, but it is not a substitute for an exact count when exactness matters. 3. Maintain a counter for hot totals For a total that is read often and written often, a separate counter can help. The usual pattern is a small table holding one row per counted thing, updated when rows are inserted or deleted, or refreshed on a schedule from a background job. This moves the cost from read time to write time or to a periodic refresh. It also introduces new trade-offs: a single counter row can become a contention point under heavy writes, and a scheduled refresh means the displayed total lags behind reality. It suits dashboar...

Powered by Readybot.io

**DEV Community** ## Why Is COUNT(*) So Slow on Large …

DEV Community

Kubecost vs. OpenCost in 2026: Production Architecture & Setup for Kubernetes FinOps

As containerized workloads scale across enterprise production clusters, understanding where container spend goes is one of the hardest challenges in cloud financial management. Traditional cloud billing tools like AWS Cost Explorer or Azure Cost Management stop at the node level. They can tell you how much an EC2 instance or an EKS node pool costs, but they are entirely blind to what is happening inside the cluster: which microservices, Kubernetes namespaces, deployments, or individual engineering teams consumed that compute. In our foundational guide on Kubernetes Cost Allocation & Showback in 2026, we explored why accurate container showback is non-negotiable for modern engineering squads. To achieve this granularity, platform engineers generally choose between two primary open-standards-based tools: OpenCost (the CNCF open-source standard) and Kubecost (the commercial enterprise tier built on top of OpenCost). In this technical deep dive, we will compare their production architectures, resource footprints, and setup patterns to help you choose the right stack for your cluster. Architectural Overview: The Shared Core Before comparing features, it is vital to understand their relationship. OpenCost was originally created by Kubecost and later donated to the Cloud Native Computing Foundation (CNCF). At their core, both tools rely on the same Prometheus metrics pipeline to scrape cAdvisor and Kubernetes API server data, mapping CPU, memory, persistent storage, and network egress down to individual pods. OpenCost: A lightweight, 100% open-source Prometheus-exporter daemon designed specifically for real-time cost allocation, multi-cloud pricing integration, and basic namespace-level reporting without licensing overhead. Kubecost: Built on top of the Open Cost engine, Kubecost adds an enterprise control plane, advanced access control (RBAC), multi-cluster aggregation, automated governance alerts, and direct integrations with enterprise ticketing and billing systems. Step 1: Deploying Open Cost via Helm in Production If your primary goal is clean, zero-cost namespace show back backed by your existing Prometheus instance, Open Cost can be deployed in minutes via its official Helm chart. Below is a production-hardened Helm values configuration that secures the deployment and points it to your cluster's Prometheus endpoint: opencost-production-values.yaml exporter: costModel: prometheusServerEndpoint: "http://prometheus-k8s.monitoring.svc.cluster.local:9090" cloudProvider: "aws" clusterId: "production-eks-cluster-01" resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1000m memory: 1024Mi service: type: ClusterIP port: 9003 persistence: enabled: true storageClass: "gp3" size: 10Gi Deploy this chart into your monitoring namespace using Helm: helm repo add opencost https://opencost.github.io/opencost-helm-chart helm repo update helm upgrade --install opencost opencost/opencost \ --namespace monitoring \ -f opencost-production-values.yaml Step 2: Evaluating the Kubecost Enterprise Upgrade Path While OpenCost handles single-cluster allocation exceptionally well, platform teams managing multiple EKS, GKE, or AKS clusters often hit architectural limitations. You should consider upgrading from OpenCost to Kubecost if your enterprise environment requires: Cross-Cluster Federation: Aggregating cost data across 10+ disparate Kubernetes clusters into a single unified global dashboard. Enterprise RBAC & SSO: Restricting engineering teams so they can only view cost allocations for their own specific namespaces via Okta, Keycloak, or SAML integration. Advanced Recommendations: Automated, machine-learning-driven container right-sizing suggestions that output exact CPU and memory limit adjustments. Allocation Accuracy for Shared Services: Granular algorithms for splitting shared infrastructure costs (like Ingress controllers or monitoring agents) across tenant namespaces proportionally. Choosing the Right Tool for Your FinOps Stage If you a...

Powered by Readybot.io

**DEV Community** ## Kubecost vs. OpenCost in 2026: Production Architecture …

DEV Community

My nightly backup job had been green for months. I never restored it once.

My nightly backup job had been green for months. I never restored it once. Last week I looked at a cron job of mine that had printed "backup ok" every night for months, and I asked myself a plain question: when did I last load one of those files into a database? I could not remember a date. A green job tells you a file was written. It does not tell you the file works. I built a small test and ran it end to end. I created a Postgres database, backed it up on a schedule, changed the live data afterwards, restored the backup into a separate database, and compared row counts. I used Levelrail, the self-hosted deploy platform I work on, built from the v1.0.1 release branch (commit d510e8488). Everything below ran on my Mac with Docker, and I mark the parts I only read in the docs. TL;DR Step Command What it proves Schedule backups schedule set A backup runs without you Restore as new backups restore-as-new The backup loads into a fresh database Compare databases query Counts match the moment of the backup Drill backups drill run Checksum, restore and content check pass Copy backups destinations copy set A second bucket holds the same object (docs only) The test setup I made a Postgres 16 database with two tables: 500 customers and 2000 orders. Then I recorded three numbers I could check later. levelrail-cli databases create --name main --engine postgres --version 16
levelrail-cli databases query main --sql "select (select count() from customers) as customers, (select count() from orders) as orders, (select sum(total_cents) from orders) as total_cents"
customers orders total_cents
500 2000 8805000
I pick a sum on top of the counts because two tables with the right row count can still hold the wrong values. Schedule it and take one now A backup target is the place the files go. Levelrail has a local type that needs no bucket, which is fine for a first run but lives on the same disk as the database, so it is no protection against losing the machine. I used it first, then switched to an S3-compatible target, which I get to below. levelrail-cli backup-targets create --name nightly --provider local --path nightly
levelrail-cli backups schedule set main --target TARGET_ID --cron "0 3 * * *" --retain 7 --retain-days 30
levelrail-cli backups trigger main --target TARGET_ID
levelrail-cli backups list main
The list printed a succeeded row of about 11.8 KB, finished in the same second it started. Retention combines two limits: keep at most the newest 7 backups, and nothing older than 30 days. A backup goes when either rule would remove it (docs/guides/self-host-postgres-with-backups.md). Break the live data, then restore the old state This is the step that makes the test mean something. After the backup, I deleted 500 orders from the live database, the way a bad migration or a wrong DELETE would. levelrail-cli databases query main --write --confirm main --sql "delete from orders where id > 1500"
levelrail-cli backups restore-as-new main --backup BACKUP_ID --new-name main-restore
Restoring as new builds a separate database next to the original and never touches it. The new container was up in under a minute. Then I ran the same count query against both. live
customers orders total_cents
500 1500 6597750

restored
customers orders total_cents
500 2000 8805000
The restored database has the numbers from before the delete, to the cent, and the live one has the damage. That is the output I wanted from a backup, and I had never produced it from my cron job. An in-place restore also exists. It overwrites the live database and asks you to type the database name, so I would use restore-as-new first and only then decide. What a restore drill adds A manual rehearsal is good once. A drill is the same idea on a timer. For Postgres, MySQL and MariaDB, Levelrail downloads the stored backup, compares its SHA-256 with the checksum recorded at backup time, replays it into a throwaway container with no network, check...

Powered by Readybot.io

**DEV Community** ## My nightly backup job had been green …

DEV Community

My YouTube automation kept "running" for almost 6 months. The bot made 1,376 commits. Zero uploads, and I had no idea.

I run a small Python pipeline on GitHub Actions that generates and uploads Shorts for a few YouTube channels. Five scheduled workflows, including one I proudly named "Self-Healing Health Check". Last weekend I finally opened the Actions tab. Every recent run was red. Upload runs, growth jobs, and the self-healing job itself. Meanwhile the bot had pushed 1,376 automated commits since May, so from a distance the repo looked alive. Last successful upload before the fix: May 25. That is 175 days of nothing. I sat down with an AI coding agent and had it run every stage and classify each failure instead of fixing anything. Result: 14 separate problems. 4 were expired or revoked credentials 5 were upstream changes (a TTS endpoint returning 403 until I upgraded the library, a deprecated image model, a free tier that added rate limits, a removed vision model, a trends endpoint returning 404) 5 were my own code or config So 9 of 14 had nothing to do with my code. The world moved and my pipeline stayed still. The actual root cause was not in the repo at all. My Google OAuth app was still in "Testing" status, which means refresh tokens die after 7 days. Every re-auth I had ever done was a 7-day fix. The error in the logs was just invalid_grant. Fixing that had its own trap: the "Publish app" button was greyed out with a vague message about completing the Branding page, even though every required field was filled. It only unlocked after I added a home page URL, a privacy policy URL and an authorized domain. For a personal uploader script. I used a tiny public GitHub repo with a README and PRIVACY.md. Other things I learned the hard way that night: My self-healing workflow shared the same dead token as the jobs it was supposed to heal. It failed every time, quietly. A watchdog in the same failure domain is not a watchdog. My local repo had six months of uncommitted changes. Production was running code from May. I had been "fixing" a version that was never deployed. The agent printed my git remote URL, which had a personal access token embedded in it. Rotated it to a fine-grained token, forgot the Secrets permission, got a 403 on gh secret set, fixed that too. I asked the agent to delete temp files matching probe. The match was case-insensitive and it deleted 41 files of a finished video about the Parker Solar Probe. That one is on me for the sloppy instruction. What fixed production in the end was almost no code. It was: find the real cause, change one Google setting, re-auth once, update one secret. First green scheduled run in a long time followed a few minutes later. What I am changing: Check outcomes, not exit codes. "Did a video actually appear on the channel today" is the only health check that matters. Keep an expiry ledger: every token, key, free tier and model name, with the date it dies. Alerts go somewhere I actually look, and the alerting path must not depend on the thing it monitors. Question for people running unattended automations: how do you catch the "everything is technically running but nothing is being produced" case? Heartbeats? Output checks? Something smarter? Note: I wrote this post with AI assistance. The incident, numbers and fixes are from my own pipeline.

Powered by Readybot.io

**DEV Community** ## My YouTube automation kept "running" for almost …

DEV Community

How to Send a Password Without Leaving It in Your Chat History

You need to give a coworker the Wi-Fi password, or a family member the login for a shared account. The fastest way is to paste it into a chat. The trouble is that chat messages are built to last. They sync to every device you're signed in on, they get backed up, they show up in search, and they're still there years later when someone borrows your laptop or a backup leaks. A password pasted into a chat lives as long as the chat does. This post walks through a simple fix, the one-time secret link: how it works, the browser crypto that makes it possible, and the limits worth knowing before you rely on it. The idea: a link that works once A one-time secret link (also called burn-after-reading) turns a piece of text into a link that the recipient can open exactly once. After that it's destroyed, and if nobody opens it before it expires, it's destroyed too. What's left in the chat history is a dead link. Anyone scrolling back later finds a link that no longer opens anything. Plenty of tools offer this. The interesting part is how you build one so that the service in the middle can't read the secret either. How it works under the hood A good design has four pieces. 1. Encrypt in the browser, before anything is uploaded The sender's browser generates a random key and encrypts the text locally, using AES-GCM via the Web Crypto API. Only the encrypted bytes (the ciphertext) go to the server. 2. Put the key in the URL fragment The key goes into the link, after the #: https://example.com/s/abc123#k=Q3VyaW91cyBhcmVuJ3QgeW91
The part after # is the fragment. Browsers don't send the fragment to the server when they request the page, and they leave it out of the Referer header too. So the server stores ciphertext it has no key for, and the key travels only inside the link you send. The fragment does stay in the browser's address bar and history, and in the chat as part of the link. That's exactly the copy that goes dead once the secret is burned. 3. Burn on read, and expire When the recipient opens the link, the server hands over the ciphertext once and deletes it. If the link is never opened, a timer deletes it anyway. 4. Don't burn on page load Chat apps often fetch a link in the background to build a preview card. If merely loading the page reveals and burns the secret, a preview bot can use up the one view before your recipient ever sees it. The fix is to load the page with zero requests to the secret, and fetch the ciphertext only when a person presses an "open" button. A minimal sketch in JavaScript Here's the core of both sides using the Web Crypto API. upload and fetchAndBurn stand in for your own server calls. // helpers: bytes <-> URL-safe base64
const toB64Url = (bytes) =>
btoa(String.fromCharCode(...bytes)).replace(/+/g, "-").replace(/\//g, "").replace(/=+$/, "");
const fromB64Url = (s) =>
Uint8Array.from(atob(s.replace(/-/g, "+").replace(/
/g, "/")), (c) => c.charCodeAt(0));

// Sender: encrypt locally, upload only ciphertext, keep the key for the link
async function createLink(secret) {
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(secret));
const rawKey = new Uint8Array(await crypto.subtle.exportKey("raw", key));
const id = await upload(iv, new Uint8Array(ciphertext)); // server sees iv + ciphertext only
return https://example.com/s/${id}#k=${toB64Url(rawKey)};
}

// Recipient: runs only after the person clicks "open"
async function openLink(id) {
// base64url has no & or =, so URLSearchParams can parse the fragment safely
const k = new URLSearchParams(location.hash.slice(1)).get("k");
const key = await crypto.subtle.importKey("raw", fromB64Url(k), "AES-GCM", false, ["decrypt"]);
const { iv, ciphertext } = await fetchAndBurn(id); // server returns once, then deletes
const plain...

Powered by Readybot.io

**DEV Community** ## How to Send a Password Without Leaving …

DEV Community

Boot the Release Against a Copy of Production Before You Switch

The deploy script already had a rollback. Switch the current symlink, restart the systemd unit, poll /healthz, and if that fails, switch back and restart again. On paper that's a safe deploy. In practice, every broken release still produced an outage. The reverse proxy returned 502 from the moment the old process stopped until the rollback had brought it back — up to about 35 seconds in the worst case, and a few seconds even after the health check learned to fail fast. A post-switch health check can't prevent that. It runs after the damage, and its only job is to bound it. The fix was to move the question "does this release start?" to before the switch, and to ask it against the real data. TL;DR: Before touching current, the deploy now copies the live SQLite database with VACUUM INTO, starts the new release on a free localhost port against that copy with a throwaway config and session secret and every outbound job disabled, and waits for /healthz. If it doesn't come up, the deploy aborts with the test server's last log lines and production never notices. The first check in the preflight is whether the release is readable by the runtime group — because the preflight runs as the deploy user and would otherwise miss permission bugs entirely. A deliberately broken release was then pushed through the real pipeline: the deploy failed, the site served HTTP 200 throughout. What Does a Post-Switch Health Check Actually Protect? Less than it looks like. The sequence is: switch current to the new release restart the service — the old process is gone now poll /healthz on failure: switch back, restart, poll again Everything between step 2 and the end of step 4 is downtime if the release is broken. The health check decides whether you stay down, which is valuable, but the outage itself is already happening by the time it reports anything. What breaks a release in a small Node.js app is rarely subtle. A missing package after npm ci, a syntax error in a file the tests didn't load, a schema migration that throws on real data, a file the runtime user can't read. All of those fail within the first second of node server.js. None of them needs the release to be live to show up. They just need the release to start. How Do You Start a Second Copy Without Touching the First? Three things have to be separate from production: the port, the data, and anything the app does to the outside world. The port. Ask the kernel for one instead of picking a number: port="$(node -e 'const s=require("net").createServer().listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})')"
There's a tiny race between closing that socket and the app binding it, and on a single-tenant VM it doesn't matter. Binding to 127.0.0.1 matters more — the preflight instance must never be reachable from outside, not even for the few seconds it lives. The data. The app runs on SQLite in WAL mode, which means copying the .db file with cp while the app is running can produce a file that's missing whatever is still sitting in the -wal file, or one that's torn mid-write. VACUUM INTO asks SQLite itself for a consistent snapshot, WAL contents included, as a single self-contained file: vacuum_into() {
TARGET="$1" node -e '
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.env.DB_FILE);
db.exec("VACUUM INTO \x27" + process.env.TARGET.replace(/\x27/g, "\x27\x27") + "\x27");
db.close();
'
}
It reads while the live app keeps writing, and it's the same call the deploy already used for its pre-switch backup. Using the real data rather than a fixture is the point: migrations run on startup, and a migration that works on the test schema but fails on a production row is precisely the bug a fixture can't find. The side effects. This is the part that's easy to get wrong. A second instance of the app is a second instance of everything the app does on startup — including its scheduler. This one syncs orders from an external marketplace ev...

Powered by Readybot.io

**DEV Community** ## Boot the Release Against a Copy of …

DEV Community

Networking Essentials for Software Engineers – Part 5: How Email Actually Works

We use emails almost every day, whether it's for work or communicating with someone. Suppose you're using Gmail and want to send an email to your friend. You enter their email address, write your message, and click Send. That's all we do from our side. But have you ever wondered how Gmail knows where to send that email, especially if your friend uses Yahoo or another email provider? Just like HTTP, email also uses networking protocols to communicate between servers. Let's see how an email travels from our application to the receiver's inbox and understand the different protocols involved. How Does an Email Actually Travel? When we send an email, it doesn't go directly from our computer to the receiver's computer. It goes through email servers, which handle sending, receiving, and storing emails. But who manages these email servers? Some companies maintain their own mail servers, while others use email providers like Google, Microsoft, or Zoho. For example, suppose a company owns the domain example.com. It can set up its own mail servers to handle emails for addresses like [email protected]. Or it can use Google Workspace. Employees can still use [email protected], but Google takes care of sending and receiving their emails. So, having our own email domain doesn't mean we need to run our own mail servers. Now, let's see how an email travels when we send it from Gmail to Yahoo Mail. We'll also look at what changes when both people use Gmail or when we use a third-party email application like Thunderbird. Sending an Email from Gmail to Yahoo Mail Suppose you're using Gmail and want to send an email to your friend who uses Yahoo Mail. You open Gmail in your browser, write your message, enter your friend's email address, and click Send. Let's see what happens next. 1. Your email reaches Gmail's servers Since we're using Gmail in a browser, our email first reaches Google's servers through HTTPS, the same protocol we learned about in our previous article. Google receives the email and checks where it needs to be sent. 2. Gmail finds Yahoo's mail server Suppose your friend's email address is [email protected]. Gmail looks at the domain yahoo.com and needs to find which server receives emails for that domain. To find it, Gmail uses DNS and looks for a special record called an MX (Mail Exchange) record. An MX record tells us which mail servers handle incoming emails for a particular domain. Once Gmail finds the mail server's name, it can find its IP address and connect to it. 3. Gmail sends the email to Yahoo Now Gmail needs to transfer the email to Yahoo's mail server. For this, it uses SMTP (Simple Mail Transfer Protocol). SMTP is the protocol mail servers commonly use to send emails to each other. Gmail connects to Yahoo's mail server, sends the message, and receives a response telling it whether the email was accepted. 4. Yahoo receives and stores the email Once Yahoo accepts the email, it processes the message and delivers it to your friend's mailbox. Now, how does your friend read it? If they open Yahoo Mail in their browser, the browser uses HTTPS to communicate with Yahoo's servers and display the email. But if they're using a third-party email application like Thunderbird, it can use IMAP or POP3 to access emails from Yahoo's servers. We'll understand how IMAP and POP3 work shortly. What If Both of You Use Gmail? Now suppose you and your friend both use Gmail. When you click Send, your browser sends the email to Google's servers using HTTPS, just like in our previous example. But this time, Google doesn't need to find another email provider's mail server. Since your friend also uses Gmail, Google can handle the delivery within its own mail system and store the email in your friend's mailbox. Google may use SMTP or other methods to communicate between its internal servers, but it doesn't need to establish an SMTP connection with another email provider. What If We Use a Third-Party Email Application? So far, we've seen h...

Powered by Readybot.io

**DEV Community** ## Networking Essentials for Software Engineers – Part …

DEV Community

rsync -a Stole the setgid Bit, and systemd Blocked the Rollback

The setup was deliberately boring. A small Node.js app with SQLite on a single VM, a release-directory layout with a current symlink, a CI workflow that uploads each release over SSH, runs npm ci, flips the symlink, restarts the systemd unit and waits for /healthz. If the health check fails, flip the symlink back and restart again. Every piece of that looked right in review. The first time CI ran it end to end, the deploy went red, the app went down, and the rollback — the part that exists precisely for this moment — failed as well. Neither failure was in the deploy logic. Both were in what the tools around it did by default. TL;DR: rsync -a includes -p and -g, so it copies the sender's mode and group onto the target — including onto the release directory itself, which silently clears the setgid bit the server relied on. npm ci then wrote node_modules with the deploy user's private group, and the app, running as a different user that only shares a group, couldn't read its own dependencies. The app crash-looped into systemd's StartLimitBurst, after which systemd refused every start — including the rollback's — until systemctl reset-failed. Fix: normalise group and setgid after the upload, call reset-failed before every restart, and stop waiting on a health check once the unit has visibly crashed. Why Two Users in the First Place? Because the process that writes code to the server shouldn't be the process that runs it. The app runs as an unprivileged runtime user with no sudo and no SSH key. Deploys run as a separate deploy user whose key is restricted and whose sudo rights cover exactly two commands: restarting the unit and reset-failed (why the second one is on the list is most of this post). The two share one group, and the shared directories are set up so both can write: # once, at setup
chgrp -R app-shared /srv/app
chmod -R g+rwX /srv/app
find /srv/app -type d -exec chmod g+s {} + # new files inherit the group
The setgid bit on a directory is what makes this work without either user having to think about it: every file created inside inherits the directory's group instead of the creator's primary group. Combined with umask 0007 on both sides, everything the deploy user creates is group-readable and group-writable, and everything outside the group sees nothing. That's the property the first deploy lost. What Did rsync Actually Change? The workflow creates the release directory first, then uploads into it: ssh deploy-target "mkdir -p '/srv/app/releases/${REL}'"
rsync -az ./ "deploy-target:/srv/app/releases/${REL}/"
The mkdir happens inside releases/, which has setgid, so the new directory is created correctly: shared group, setgid set. Then rsync runs, and -a is shorthand for -rlptgoD. Two of those letters do the damage: -p copies permissions from the sender. On the CI runner, the checkout directory has no setgid bit — why would it. A trailing slash on the source (./) means rsync syncs the attributes of the directory itself as well as its contents, so it chmods the freshly created release directory to the runner's mode. setgid gone. -g copies the group. A non-root receiver can only assign groups it is a member of, so rsync falls back to the deploy user's own primary group for everything it creates. Nothing about this looks wrong in a listing unless you're looking for it. The files are there, the sizes match, the owner is the deploy user as expected. The difference is one letter in the mode column and one name in the group column. The next step made it fatal. npm ci runs on the server, as the deploy user, inside a release directory that no longer has setgid. Every file in node_modules got the deploy user's private group, with umask 0007 stripping all "other" bits. The runtime user — different UID, member only of the shared group — couldn't read a single dependency. require('express') failed on the first line of server.js. Why Didn't the Rollback Save It? Because by the time the script tried to roll back, systemd had st...

Powered by Readybot.io

**DEV Community** ## rsync -a Stole the setgid Bit, and …

DEV Community

Eliminating Redis & Valkey Hot-Key Bottlenecks in Multi-Region Distributed Rate Limiters

When a single enterprise tenant fires 250,000+ requests per second across US, UK, and European API gateways, the failure mode rarely looks like an obvious cluster crash. More often, aggregate Redis or Valkey CPU sits comfortably below 25%, yet P99 gateway latency jumps from 1.2ms to nearly 190ms. In a standard distributed token-bucket setup, an atomic Lua script runs against one central key—such as ratelimit:tenant_9841. Because Redis and Valkey execute Lua scripts single-threaded on the primary node owning that CRC16 hash slot, dozens of edge gateway pods end up queuing behind a single CPU core. (You can explore the complete architectural walkthrough and production implementation in our full guide on AI Dev Toolkit.) Why Single-Key Rate Limiters Stall at Scale Three compounding bottlenecks trigger this sudden latency cliff during heavy traffic surges: Single-Slot Serialization: Wrapping {tenant_id} inside hash tags pins every request for a high-volume customer to one primary shard, leaving the rest of the cluster mostly idle. Cross-Region NTP Clock Skew: Passing edge proxy timestamps into Lua causes inconsistent token refills across us-east-1, eu-central-1, and London edge nodes. Per-Request Network Round-Trips: Paying an inter-AZ network hop on every single HTTP request caps throughput and inflates tail latency. The 3-Layer Architecture Fix (1.45M+ RPS) To eliminate hot-key stalls while keeping strict quota accuracy, high-throughput API gateways combine three architectural shifts: Virtual Key Sharding: Place the shard index inside the hash tag (rl:tenant_882:{shard_0..15}) instead of the tenant ID so token buckets distribute evenly across 16 independent primary slots. Server-Authoritative Millisecond Timing: Anchor elapsed refill math directly to Redis or Valkey server time (redis.call('TIME')) inside Lua so edge clock drift never causes burst leaks or negative deltas. Two-Tier Hybrid Lease Batching (Edge L1 + Cluster L2): Have Go or Rust edge proxies lease micro-batches of tokens (for example, 50 tokens per lease with a short local TTL) in memory. Most checks resolve locally in nanoseconds, cutting cluster round-trips by up to 98%, backed by a fail-open local sliding window if inter-region RTT spikes. What Multi-AZ Benchmarks Show In multi-AZ edge tests simulating a single hot tenant at 300,000 RPS: A single key with per-request Lua execution saturates one primary thread and pushes P99 overhead to 188.4 ms (~285k RPS). Distributing across 16 virtual shards balances CPU load across primaries and drops P99 overhead to 4.8 ms (~920k RPS). Combining 16-shard keys with edge L1 lease batching cuts rate-limit overhead to 0.42 ms while sustaining 1.45M+ RPS with linear horizontal scale. Read the Complete Production Guide For the full Lua token-bucket implementation, EVALSHA deployment checklist, deterministic PEXPIRE memory bounds, and complete multi-region failover playbook, read the full engineering guide on AI Dev Toolkit: 👉 Read the full guide: https://aidevtoolkit.blogspot.com/2026/11/distributed-systems-api-gateway-redis.html

Powered by Readybot.io

**DEV Community** ## Eliminating Redis & Valkey Hot-Key Bottlenecks in …

DEV Community

Regression tests for GROWI-style revision authorization

Regression tests for GROWI-style revision authorization Overview CVE-2026-105272 came from two IDs that looked related to the application but were not proven related to each other. GROWI authorized a page ID, then accepted a revision ID from a different page. The fix in v8.0.7 turns that relationship into an explicit condition. A useful regression suite should test mixed ownership, not only valid and invalid sessions. The security property is that every request binds a child object to the same parent used for authorization. JVN lists the affected range as GROWI v3.2.10 through v8.0.6 and assigns CVSS 4.0 2.3 and CVSS 3.0 4.2 scores. Mechanism and exploitation conditions The vulnerable read paths accepted pageId and revisionId. Authorization checked the page. The revision was fetched without confirming that it belonged to that page. A logged-in user could combine a viewable page with a foreign revision and receive the foreign body. The comment endpoint could return foreign comments. Comment create and update paths accepted foreign revisions for linking. The attacker needed a valid login and a target revision ID. MongoDB ObjectId values contain time, process randomness, and a counter. They are not secret by design. A user can create revisions and observe their shape. The page route did not require edit permission. Share-link access was not affected. Impact A missing test for object ownership can expose restricted page bodies, comments, and creator information. The maintainer says the page read creates no revision and leaves no trace in that path. No reviewed source reports exploitation in the wild. The issue is a confidentiality bypass with a limited comment-linking integrity problem. Affected products and scope JVN lists GROWI v3.2.10 through v8.0.6. The page-body path has been present since v3.3.0, and the comment endpoint accepted revision IDs from at least v3.2.10. The fixed release is v8.0.7. Exposure context ZoomEye returned 401 product-title matches for GROWI on 2026-11-15. The exact vul.cve="CVE-2026-105272" Filter returned zero. These results are inventory evidence only. Version and route testing are still required. Remediation and mitigations GROWI v8.0.7 checks that a supplied revision belongs to the page. Page-body and comment reads fall back to the page's own latest revision or comments when ownership fails. Comment writes reject the foreign revision. The advisory lists no workaround. A regression test based on the advisory should cover a page and revision owned by the caller, a revision that exists on another page, and a revision ID that does not exist. The first should render the expected page data. The second and third should not disclose the foreign body or comments and should follow the same fallback path. Repeat for comment reads and for comment create and update operations. Check a read-only account and share-link behavior as separate cases. Do not assert that an empty application log proves no read occurred. References JVN#51223238 GROWI advisory GHSA-m72c-xgjf-56ww GROWI v8.0.7 release OSV CVE-2026-105272

Powered by Readybot.io

**DEV Community** ## Regression tests for GROWI-style revision authorization Regression …