---
title: "Tests All Green but Inventory Oversold? Using Bruno to Achieve \"Programmatically Generated\" E-commerce API Integration Tests."
description: "Every API test is green, yet the customer got charged twice. This article runs a real Bruno integration suite against an e-commerce API: reconcile like an accountant instead of comparing fields, query the database straight from Node.js scripts, and generate hundreds of tests with code. All screenshots are from actual runs."
canonical_url: "https://blog.markkulab.net/en/post/bruno-generated-ecommerce-api-integration-testing"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/en/author/mark-ku"
site: "Mark Ku's Tech Notes"
date_published: "2026-08-07 20:18:37 +0800"
category: "Tech Sharing"
language: "en"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "when reusing or quoting, credit the author and link back to the original"
---

# Tests All Green but Inventory Oversold? Using Bruno to Achieve "Programmatically Generated" E-commerce API Integration Tests.

## 1. Introduction: Every Test Is Green, So Why Was the Customer Charged Twice?

Here is a scenario everyone has lived through.

Every API test returns `200 OK`, every field matches the schema, and the CI report is a wall of green. Then support starts getting complaints: a user's connection hiccupped, they clicked twice, and their store credit was deducted twice. Or an item that was already out of stock somehow got sold.

Where does it break? **A single API test only proves that one call answered correctly. Real business logic is a chain of actions.**

So the scary bug is never the API returning 500, because you spot that instantly. The scary one is every API dutifully returning 200 while the customer's money and your inventory refuse to add up.

Our earlier post, [Goodbye Postman's Pricing Traps! A Practical Guide to Bruno, the Open-Source, Git-Native API Testing Tool](https://blog.markkulab.net/post/bruno-postman-alternative-guide), covered tool choice and basic setup. This one goes a layer deeper: how to move from "testing one endpoint" to "testing a whole flow," and how to make that scale.

## 2. What Actually Blocks Us in the AI Era: Not Writing, but Verifying

The biggest change this past year is that **writing code is no longer the bottleneck**. A requirement arrives and AI gets an endpoint or a page 80% right within the hour.

What has not sped up at all is verifying any of it. So the bottleneck simply moved downstream.

The faster you ship, the more combinations there are to check. Clicking through a UI and eyeballing database rows is a linear cost, and it cannot keep up. What deserves the investment is a verification loop that is **repeatable, generated by code, and runnable in CI**.

I evaluated the options and settled on Bruno, for four plain reasons:

| What I needed | What Bruno gives |
| :--- | :--- |
| Easy for AI to edit | `.bru` is plain text, so AI can read it, change it, and generate a hundred of them. Postman's giant JSON blob cannot do that |
| Can query the database | Test scripts are just Node.js, so `require` a DB driver and run SQL |
| Fits into CI | An official CLI [2] with HTML / JSON / JUnit reporters, plus an official Docker image [3] that drops straight into GitLab CI |
| Version-control friendly | The files are the tests, `git diff` is readable, no cloud account or paid seats |

The whole methodology is three steps, and the order matters:

```mermaid
---
title: The three-step verification
---
flowchart LR
  A["Call the API<br/>run the flow"] --> B["Check the response<br/>did it say it worked"]
  B --> C["Ask the database<br/>did it actually do it"]
```

Step one catches broken APIs. Step two catches APIs returning wrong data. Step three catches the sneakiest kind: **the API returns 200 with correct fields, but the books in the database are wrong**. Later in this article a real run proves that only step three stops it.

> None of these screenshots are mockups. I wrote a Node.js + SQLite fake e-commerce API as a target, and every image below is this suite actually running against it.

## 3. The Core Idea: Don't Compare Fields, Reconcile the Books

If an integration test just walks down the response fields one by one, you miss the forest for the trees. What you should really be validating is the system's **invariant**, or in plain words, the equation that must hold no matter what [6].

### Test Like You Balance a Wallet

Picture going shopping:

1. **Count your wallet before leaving** and write the number down (call it B0, the starting value).
2. **Go shop**: place an order, pay, return one item, cancel another.
3. **Count the wallet again** and check that the amount missing is exactly what you should have spent.

As a test, that looks like:

```mermaid
---
title: "Test like you balance a wallet: check the delta, not the remainder"
---
flowchart TD
  A["1. Count the wallet first<br/>read starting value B0"] --> B["2. Go shopping<br/>order / pay / refund / cancel"]
  B -->|what the system actually did| C["3. Count it again<br/>read ending value B1"]
  B -->|what the books say it should cost| E["Expected change Δ<br/>every action's amount, summed"]
  C --> D{"Does B0 - B1 equal Δ?"}
  E --> D
  D -->|Yes| G["Green<br/>this round's books balance"]
  D -->|No| R["Red<br/>double charge / undercharge / missing refund"]
```

### Record the Difference, Not the Remainder

It feels natural to assert `balance == 1000`. But the moment the environment is shared, someone else touches a row and your test goes red.

Record the delta instead, for example `balance == B0 - 600`. Now it passes whether the starting balance is 1000 or 5000. The test stops caring about polluted data and about execution order.

### Three Practical Rules

1. **Every scenario reads its own starting value.** No shared globals: each test folder calls the API itself at the start, so scenarios never interfere.
2. **Round decimals before comparing.** E-commerce is full of discounts and tax rates. Run both sides through `Math.round(x * 100) / 100` so floating-point noise never turns a test red.
3. **Put the expected number in the assertion name**, for example `Verify balance (expected 850)`. When it fails, the report already tells you the gap and you never open a log.

## 4. Bruno's Three Key Features

To run the loop above, the tool has to pass state around and organize structure. Open-source Bruno [1] does both well.

### Pass the Previous Step's Result to the Next

Integration testing is a relay, known as request chaining [5]. Bruno passes values between requests with `bru.setVar` and `bru.getVar`.

Read the balance first and store it:

```javascript
// Read the starting balance and save it as a variable
bru.setVar('s01_bal0', Number(res.getBody().member.balance));
```

In the final step, compute what it should be, then let the declarative `assert` compare:

```javascript
// Compute the expected value in pre-request
const b0 = Number(bru.getVar('s01_bal0'));
bru.setVar('s01_expBal', Math.round((b0 - 600) * 100) / 100);
```

```
// Compare in the assert block
assert {
  res.status: eq 200
  res.body.member.balance: eq {{s01_expBal}}
}
```

Complex maths in code, final comparison declarative. Clean reports, full flexibility.

### One Shared Script for the Whole Collection

Some work repeats in every request, like logging in for a token. Instead of duplicating it, lift it to the top of the collection:

* **Compute the token once**: every later request reuses it, no repeated login flow.
* **A safety gate**: integration tests really do mutate data, so they must never hit production. Add this at the collection level:

```javascript
// Collection-level script:pre-request
const url = (bru.getEnvVar('baseUrl') || '').toLowerCase();
if (!url) {
  throw new Error('No baseUrl - select the UAT environment first.');
}
if (/prod|production|live/.test(url)) {
  throw new Error('BLOCKED: baseUrl looks like production. UAT only!');
}
```

This gate is not decoration, it really stops the run. Below is what happened when I forced the URL to something that looks like production: the very first request is aborted and not one row is written.

![Bruno CLI output showing the collection-level security gate blocking a production baseUrl](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-production-safety-gate.webp)

Pair it with the rule "never create a production environment file" and mistakes become very hard to make.

### Folders Are the Test Structure

Bruno's `.bru` files carry a `seq` that sets execution order, and folders naturally group scenarios. One business scenario per folder:

```
requests/
├─ 00-launch/            # Create the session everything else needs
├─ 02-shop-a/            # Platform A
│   ├─ 01-regular-checkout/  # Buy something normally
│   │   ├─ 1-read-stock.bru  # Record the stock starting value
│   │   ├─ 2-read-balance.bru# Record the balance starting value
│   │   ├─ 3-create-order.bru
│   │   ├─ 4-pay.bru
│   │   ├─ 5-ship.bru
│   │   ├─ 6-verify-stock.bru
│   │   └─ 7-verify-ledger.bru # Check balance + database
│   ├─ 02-duplicate-payment/
│   └─ 03-out-of-order/
└─ 03-shop-b/            # Platform B, same six scenarios, different rules
```

Bruno walks the folders in order, so the report structure looks exactly like the business scenarios. Very easy to read.

## 5. Six Scenarios Where Things Actually Break

Don't write integration tests by walking down the API spec. Write them by walking down the ways the system breaks. In e-commerce, the damage comes from double clicks, network lag, and rejected requests.

Borrowing from industry practice such as Stripe's idempotency design [4], here are six scenarios:

| Category | What it tests | The real incident it prevents |
| :--- | :--- | :--- |
| **Normal flow** | Order → Pay → Ship | Broken basics, or wrong stock and amount maths |
| **Duplicate submission** | The same payment (same receipt number) sent twice [4] | **Double charging** from double clicks or webhook retries |
| **Out-of-order arrival** | A shipment request arrives before the payment confirmation | Network lag scrambles the state machine and unpaid orders ship |
| **Amount variance** | Cancellations, partial refunds, shipping fees and coupons | Wrong refund maths, disagreement over refundable fees |
| **Rejected (stock)** | Ordering 7 units when only 2 remain | Overselling, and rejected requests quietly mutating data |
| **Rejected (balance)** | A wallet holding 120 buying a four-figure item | Negative balances, or a ledger row written for a rejected order |

A quick plain-language note on idempotency: it means **doing the same action several times should produce the same result**, like pressing an elevator button ten times and still getting one elevator. Payment APIs implement it by having the client send a receipt number (an Idempotency Key). When the server sees a repeat, it replays the first result instead of charging again.

Six scenarios times two platform profiles is the whole demo. Here it is running:

![Bruno CLI run with 55 requests and 85 assertions all passing](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-cli-full-run-green.webp)

55 requests, 55 tests, 85 assertions, under 0.4 seconds. Speed is not the point. The point is that **all 140 checkpoints can be re-run on every commit for free**. That is the difference between systematic verification and clicking around.

## 6. Stop Hard-Coding Numbers into Tests

The fastest way to rot an integration suite is magic numbers:

```javascript
// Three months later nobody knows where 600 came from
expect(res.getBody().member.balance).to.equal(b0 - 600);
```

Switch platform or tweak a shipping rule and every one of those numbers is wrong, silently.

The fix is to lift "how this platform calculates" into a **rules table** and derive the expectations from it:

```json
{
  "shop-a": {
    "cancelSemantics": "refund-all",
    "insufficientStockBehavior": "block-order",
    "shippingFee": 60,
    "taxRate": 0
  },
  "shop-b": {
    "cancelSemantics": "keep-shipping-fee",
    "insufficientStockBehavior": "block-order",
    "shippingFee": 80,
    "taxRate": 0.05
  }
}
```

In plain words: Shop A refunds everything on cancellation, charges 60 for shipping, no tax. Shop B keeps the shipping fee, charges 80, and adds 5% tax.

Then write the maths as plain functions shared by the whole collection:

```javascript
// lib/expectations.js
const round2 = (n) => Math.round(n * 100) / 100;

// amount = goods x (1 + tax) + shipping - coupon
function orderAmount(profile, unitPrice, qty, coupon) {
  const goods = round2(unitPrice * qty);
  return round2(goods * (1 + profile.taxRate) + profile.shippingFee - (coupon || 0));
}

// How much comes back on cancel depends on this platform's rule
function cancelRefund(profile, amount) {
  return profile.cancelSemantics === 'keep-shipping-fee'
    ? round2(amount - profile.shippingFee)
    : round2(amount);
}

module.exports = { round2, orderAmount, cancelRefund };
```

Now the tests contain no hard-coded numbers, only "given this platform's rules, the answer should be":

```javascript
// script:pre-request
const P = { cancelSemantics: 'refund-all', shippingFee: 60, taxRate: 0 };
const exp = require(bru.cwd() + '/lib/expectations.js');
bru.setVar('s01_expAmount', exp.orderAmount(P, Number(bru.getVar('s01_price')), 2, 100));
```

Two benefits. First, onboarding a platform means swapping the rules table, with zero test edits. Second, the derived number lands in the assertion name, for example `Refund follows profile.cancelSemantics=keep-shipping-fee (expected 189)`. When it goes red, you immediately know whether I misread the rule or the system miscalculated.

## 7. Don't Hand-Write Tests One by One

By now the suite is highly regular: six scenarios, a fixed three-step check, expectations derived from a rules table. Regular enough that hand-writing it is a waste.

A Postman collection is one huge JSON document. Code can emit it, but nobody can review the diff. Bruno's `.bru` is plain text, so a string template is enough:

```javascript
// generate.mjs (excerpt)
function req({ name, seq, method, url, body, assert = [], tests }) {
  const parts = []
  parts.push(`meta {\n  name: ${name}\n  type: http\n  seq: ${seq}\n}`)
  parts.push(`${method.toLowerCase()} {\n  url: ${url}\n  body: ${body ? 'json' : 'none'}\n  auth: inherit\n}`)
  if (body) parts.push(`body:json {\n${indent(body)}\n}`)
  if (assert.length) parts.push(`assert {\n${assert.map((a) => `  ${a}`).join('\n')}\n}`)
  if (tests) parts.push(`tests {\n${indent(tests)}\n}`)
  return parts.join('\n\n') + '\n'
}
```

Onboarding another partner drops from "rewrite the suite" to "fill in a rules table":

![Test generator turning two profiles into 12 scenario folders and 55 .bru requests](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-generator-output.webp)

That is the generator actually running: two rules tables in, 12 scenario folders, 55 `.bru` files and 75 files total out, in under a second.

The benefits compound:

* **A new platform costs almost nothing**: coverage lands immediately, nobody rewrites anything.
* **Everything looks the same**: identical folder structure, variable names and assertion wording, so reports compare side by side.
* **The methodology upgrades in bulk**: want every scenario to also check the order ends up SHIPPED? Change one line in the generator, regenerate, 55 tests updated.

This is what I really mean by Bruno being "AI-friendly." Plain text lets AI and scripts rewrite tests at scale, and because the tests execute, a bad rewrite turns red immediately.

## 8. The API Says It Worked, but Did the Database Record It?

So far we have only been trusting what the API says.

A correct balance response does not prove the **transaction record** was written correctly. A missing reconciliation row or a duplicated entry is invisible at the API layer.

This is where Bruno's Node.js scripting earns its keep: the test can talk to the database directly.

```javascript
// lib/ledger.js
const { createRequire } = require('module');

function openDb(collectionPath, dbRelPath) {
  const nodeRequire = createRequire(collectionPath + '/bruno.json');
  const { DatabaseSync } = nodeRequire('node:sqlite');
  return new DatabaseSync(collectionPath + '/../' + dbRelPath, { readOnly: true });
}
```

This demo uses SQLite; a real project swaps in `mssql`, `mysql2` or `pg` with the same shape, only the connection string changes.

Then define four database-level checks, numbered C1 to C4:

| Code | In plain words | What it catches |
| :--- | :--- | :--- |
| **C1 A charge needs a record** | Every successful charge appears in the ledger | Money moved with nothing to reconcile against |
| **C2 Nothing appears from nowhere** | Every ledger row traces back to an order the test created | The system quietly writing unknown transactions |
| **C3 One event, one row** | At most one row per order per action | Idempotency broken at the database layer |
| **C4 The totals must agree** | Ledger total equals the money the wallet actually lost | **The API and the ledger telling different stories** |

Hang them on the last request of each scenario and one request does all three jobs: call the API, check the response, confirm the database.

![Bruno request asserting the API response and the C1 to C4 database ledger invariants](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-db-ledger-invariants.webp)

### C4 Is the Real Gatekeeper

The first three still only ask whether the system contradicts itself. C4 is the lethal one, because it compares two independent facts: **the ledger total must equal the amount the balance actually dropped by**.

I deliberately removed idempotency from the fake server, simulating a real regression where someone reworks the payment flow and forgets to check the receipt number, then re-ran the duplicate-payment scenario:

![Red run after disabling idempotency: API returns 200 while the ledger net disagrees with the balance delta](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-double-charge-regression.webp)

Look at the details, because the support incident from the introduction is fully reproduced here:

* Both payment calls **returned 200**, with the correct amount.
* The database's unique index blocked the second ledger row, so **C3 "one event, one row" is still green**.
* But the member was charged twice: `AssertionError: expected -380 to equal -760`. The ledger says this order moved 380, while the wallet lost 760.

A suite that stops at "check the API response" ships this bug. **Only pulling the database into the reconciliation catches it in CI.**

## 9. Where Secrets Live and How to Wire Up CI

Integration tests touch real credentials, so settle the rules before wiring CI.

### Where Secrets Are Read From, in Order

1. **System environment variables** (`bru.getProcessEnv('SHOP_API_KEY')`): what CI uses, injected from GitLab CI/CD variables (masked and protected).
2. **A project-root `.env`**: local development.
3. **The Bruno environment file**: non-sensitive defaults only, such as `baseUrl`.

```javascript
// Collection level: CI reads the process env, local falls back
bru.setVar('apiKey', bru.getProcessEnv('SHOP_API_KEY') || bru.getEnvVar('apiKey'));
```

### Three Rules You Don't Break

* Secrets never enter Git; the repo keeps only `.env.example`.
* Secrets never go on the command line, or they show up in process lists and CI logs.
* Reports (HTML / JSON) contain request and response bodies, so they go in `.gitignore` or get filtered with flags such as `--reporter-skip-headers`.

### Wiring Up GitLab CI

Add `SHOP_API_KEY` as a Masked variable under Settings → CI/CD → Variables. The runner injects it as an environment variable, so `.gitlab-ci.yml` never repeats it:

```yaml
stages:
  - test

api-integration:
  stage: test
  image: node:22
  script:
    - cd collection
    - >
      npx @usebruno/cli run requests -r
      --env uat --sandbox developer
      --reporter-html ../report.html
      --reporter-junit ../junit.xml
  artifacts:
    when: always
    expire_in: 1 week
    paths:
      - report.html
      - junit.xml
    reports:
      junit: junit.xml
```

`artifacts:reports:junit` lets GitLab surface failures straight in the merge request's test summary, and `--reporter-html` produces the report you can hand to non-engineers (kept under `paths`, downloadable from the job page):

![Bruno HTML report dashboard showing 55 requests and 140 checkpoints passing](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-html-report.webp)

Two things are easy to forget. First, `when: always` is mandatory: without it a red suite fails the job before the artifacts are collected, so the report is missing exactly when you need it. Second, Bruno CLI exits non-zero on failure and that turns the job red, which is what you want; if you only want to observe for a while without blocking merges, add `allow_failure: true` instead of swallowing the command with `|| true`.

There is also an official Docker image [3] if you would rather not manage Node versions in CI: swap `image:` for it and you are done.

## 10. Traps We Hit

| # | What bit us | The fix |
| :--- | :--- | :--- |
| 1 | Guessing the rules. The doc said "cancel refunds everything," reality kept the shipping fee | Confirm every rule with one real call; never trust the doc |
| 2 | Reading state from an error response, like the stock number in a 422 body | Error paths check status and error code only; re-`GET` the state |
| 3 | Looks successful but did nothing. The duplicate payment returns 200 and charges nothing | Catch it with the final balance and ledger totals, not the intermediate 200 |
| 4 | 17-digit order ids losing precision through `JSON.parse` | Always `String()` the id; check its length, not its numeric value |
| 5 | Running a subfolder alone and failing everything because `00-launch` never created the session | Include `00-launch` in the run, or cache the token at collection level |
| 6 | Floating point: `567 * 1.05` disagreeing by a hair | Round both sides with `Math.round(x * 100) / 100` first |
| 7 | Mojibake when PowerShell 5.1 runs the helper scripts | Keep `.ps1` English-only, move prose into Markdown |
| 8 | Bruno's sandbox cannot load `node:sqlite` | Use `require('module').createRequire(...)`, or a regular npm driver |

Number 8 bit us during this write-up. When Bruno decides whether something is a Node builtin, it strips the `node:` prefix before checking its list, but `node:sqlite` happens to be listed *with* the prefix. So it gets misclassified as an npm package and the file lookup fails. One line of `createRequire` fixes it.

Which is a reminder: **what a sandbox can and cannot do must be confirmed by running it, not by guessing**.

## 11. Conclusion: Automate the Defense, but the Boundaries Stay Human

Back to the opening question. What changes here is not that tests get written faster, but that **verification stops being a one-off action and becomes a line of defense you can re-run**.

| Metric | Measured in this demo |
| :--- | :--- |
| Platform rules tables | 2 |
| Scenario folders | 12 (6 scenarios × 2 platforms) |
| Generated `.bru` requests | 55 |
| Checkpoints (tests + assertions) | 140 |
| Full suite runtime | 0.4 seconds |
| Cost of adding a platform | One rules table |

One thing needs saying plainly: **automation accelerates the defense, but not the boundaries**.

A generator can turn 55 tests into 550. AI can write assertions faster and cleaner than you. CI can run them on every push.

But whether this platform should refund the shipping fee, whether a duplicate payment counts as an error, whether a ledger mismatch blocks a release: those **boundaries** are yours to decide. Define one wrong and the machine will very efficiently verify the wrong rule a thousand times.

Tools will change, and Bruno will not be the last one. What survives is the methodology: reconcile instead of comparing fields, design scenarios around failure modes, derive expectations from rules, and always **ask the database once more after the API answers**.

That ordering is what this article is really about.

```mermaid
flowchart LR
  A["Call the API"] --> B["Check the response"] --> C["Ask the database"]
```

## References

1. [Bruno Official Site](https://www.usebruno.com/)
2. [Bruno CLI](https://blog.usebruno.com/bruno-cli)
3. [Official Bruno Docker Image and GitHub Action](https://blog.usebruno.com/official-bruno-docker-image-and-github-action)
4. [Stripe: Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency)
5. [Request Chaining - Bruno Docs](https://docs.usebruno.com/v2/testing/script/request-chaining)
6. [Invariant Test - Cyfrin Glossary](https://www.cyfrin.io/glossary/invariant-test)

---

## About this article and its author

Originally published on [Mark Ku's Tech Notes](https://blog.markkulab.net/en/post/bruno-generated-ecommerce-api-integration-testing)

License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — when reusing or quoting, credit the author and link back to the original

### About the author

**[Mark Ku](https://blog.markkulab.net/en/author/mark-ku)** — Software Solution Provider

- 10+ years senior software engineer, now an AI Builder
- Focused on large-platform architecture — North-American e-commerce, AI SaaS subscription billing
- Combining AI Agents and automation to build evolvable product foundations

### Free tools built by the author

All of these are free to use:

- [Free PDF Sign Tool](https://blog.markkulab.net/en/tools/pdf-sign): Online PDF sign tool — draw, type, or upload a signature, then drag, resize, and download. Everything runs in your browser; nothing is uploaded.
- [VS Code Refactory](https://blog.markkulab.net/en/tools/refactory): Refactory is a VS Code refactoring extension: 34 actions plus a 37-rule code-smell inspection layer with a Code Health dashboard, across 18 languages, backed by 534 tests. It learns your repo's conventions: where interfaces live, where DI is registered, whether 'use client' belongs. It ranks files by git churn × complexity so you know what to fix first, and hands any smell to the Claude Code already on your machine. Free to use, and your source never leaves your computer.
- [DB-Kit Database Manager](https://blog.markkulab.net/en/tools/db-kit): DB-Kit is a lightweight, cross-platform database manager built with Tauri + Rust + React. Manage MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, MongoDB, Redis, Kafka, Elasticsearch and RabbitMQ from one consistent interface: passwords encrypted in the OS keychain, SSH tunnels, full CRUD, a visual query builder, stacked multi-statement result sets, cross-connection data transfer and compare/sync, Excel / CSV import & export, visualized execution plans, ER diagrams, scheduled backups, SQL stress testing with p50–p99 latency percentiles, a 15-rule SQL review engine, Kafka message browsing with monitoring & alerts, a bilingual UI (Traditional Chinese / English), a built-in AI assistant (natural-language SQL, AI review and tuning advice) and the dbk CLI. Free and open source (MIT), with installers for Windows, macOS and Linux.
- [VS Code Super Mermaid](https://blog.markkulab.net/en/tools/super-mermaid): Super Mermaid is a VS Code extension for beautiful Mermaid diagrams out of the box: auto-colored live preview, mouse pan & zoom, high-res PNG / SVG export, 21 templates and multiple themes. Free and open source (MIT).
- [React Super Mermaid](https://blog.markkulab.net/en/tools/react-super-mermaid): react-super-mermaid is an open-source React component library: render beautiful Mermaid diagrams with a single <MermaidViewer>, with built-in colorful / sketch themes, pan & zoom, in-diagram search, and high-res SVG / PNG export. Lightweight, SSR-safe, fully typed. Free and open source (MIT).
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/en/tools/jira-super-mermaid): An Atlassian Forge app: write Mermaid syntax directly inside a Jira issue or a Confluence page and get flowcharts, sequence diagrams, state machines and Gantt charts. 11 diagram types, SVG / PNG export, light and dark themes, full CJK support. Runs on Atlassian: your diagrams live in your own site and the app calls no third-party service. Free, coming soon to the Atlassian Marketplace.
- [Mermaid Live Preview](https://blog.markkulab.net/en/tools/mermaid-preview): Write Mermaid in your browser, see it render instantly, and share the whole diagram as a single link. No sign-up, nothing uploaded to a server, and mermaid.live share links work as-is.
- [React Intl Phone Number](https://blog.markkulab.net/en/tools/react-intl-phone-number): react-intl-phone-number is an open-source React component: framework-agnostic and antd-free, with E.164 in/out, a searchable flag / country-code dropdown, configurable validation levels (strict / mobile-strict / loose), themeable CSS, and i18n — phone logic powered by google-libphonenumber. Lightweight and fully typed. Free and open source (MIT).
- [Uptime Kuma Cluster](https://blog.markkulab.net/en/tools/uptime-kuma-cluster): Turn single-node Uptime Kuma into a highly available cluster: OpenResty + Lua smart load balancing, shared MariaDB state, health checks and automatic failover, plus cluster-management REST APIs. One Docker Compose command to start. Free and open source (MIT).
- [Special Education](https://blog.markkulab.net/en/education): Learning materials crafted for special education students

### Daily podcasts

- [Mark's Tech Insights — Daily AI News](https://blog.markkulab.net/en/category/tech-news): Daily curated AI and tech trends. Catch the latest developments via audio summaries — covering AI applications, software architecture, DevOps, and engineering practice. — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/en/category/ai-stock-chat): Every trading day, an AI-analyzed take on the Taiwan stock market, delivered as a two-host conversation covering the session and the next-day outlook. — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml
- [開源好物週報](https://blog.markkulab.net/en/category/open-source-weekly): A weekly two-host pick of free open-source tools surfaced from real Hacker News, GitHub, and Reddit buzz — what pain they solve and the fastest way to get started. — RSS: https://blog.markkulab.net/open-source-weekly/feed.xml

### Newsletter

[Subscribe to the newsletter](https://blog.markkulab.net/en/subscribe) — Be the first to know about new posts. No spam, unsubscribe anytime.
