diff --git a/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png b/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png
new file mode 100644
index 0000000..7881543
Binary files /dev/null and b/public/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png differ
diff --git a/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png b/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png
new file mode 100644
index 0000000..c8784a7
Binary files /dev/null and b/public/og/vetting-an-open-source-dependency-before-you-bet-on-it.png differ
diff --git a/scripts/banner-gen/generate.mjs b/scripts/banner-gen/generate.mjs
index 9a904c9..c997908 100644
--- a/scripts/banner-gen/generate.mjs
+++ b/scripts/banner-gen/generate.mjs
@@ -706,6 +706,68 @@ BANNERS['adding-english-mode-to-a-chinese-only-web-app'] = {
],
};
+BANNERS['vetting-an-open-source-dependency-before-you-bet-on-it'] = {
+ titlebar: 'root@dsm — due diligence · 6 checks',
+ lines: [
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'curl -s api.github.com/repos/getcoherence/openpartner' },
+ { t: 'dim', text: 'MIT · created 2026-04 · 8 stars · 15 open issues' },
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: "grep -in 'oidc\\|sso\\|saml' README.md ARCHITECTURE.md docs/*.md" },
+ { t: 'err', text: '0 hits ← the spec assumed OIDC for partner login' },
+ { t: 'hl', text: 'Community: API tokens ✓ · SSO ✗ (Enterprise only)' },
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'selfhost mode · manual payouts · pin the commit' },
+ { t: 'ok', text: '→ 6 assumptions corrected before integration ✓' },
+ ],
+ flow: [
+ { n: '1', label: '1,306-line spec' },
+ { n: '2', label: 'repo vital signs' },
+ { n: '3', label: 'grep the feature' },
+ { n: '4', label: 'tier + rail fit' },
+ { n: '5', label: 'fallback decided ✓' },
+ ],
+};
+
+BANNERS['authentik-forward-auth-gate-wasnt-live'] = {
+ titlebar: 'root@dsm — forward-auth · two ingress layers',
+ lines: [
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: "curl -sk -H 'Host: app.example.com' https://localhost/" },
+ { t: 'err', text: '302 → /outpost.goauthentik.io/start ← looks gated' },
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'curl -sI https://app.example.com/' },
+ { t: 'err', text: 'x-powered-by: Express ← the app answered, not the outpost' },
+ { t: 'hl', text: 'the tunnel rule went straight to the container — nginx never saw the request' },
+ { t: 'dim', text: 'grep -c "proxy_pass :10000" → 11 vhosts share that port' },
+ { t: 'cmd', text: 'verify from outside · read which layer replied' },
+ { t: 'ok', text: '→ one line to revert · ten gates left intact ✓' },
+ ],
+ flow: [
+ { n: '1', label: 'vhost → outpost' },
+ { n: '2', label: 'tunnel → app', err: true },
+ { n: '3', label: 'x-powered-by told' },
+ { n: '4', label: 'count the port' },
+ { n: '5', label: 'gate verified ✓' },
+ ],
+};
+
+BANNERS['nocodb-sso-is-a-licensed-feature'] = {
+ titlebar: 'root@dsm — nocodb CE 2026.09.0',
+ lines: [
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'NC_SSO=oidc NC_SSO_OIDC_ISSUER=https://auth…' },
+ { t: 'dim', text: 'enforced at boot — the vars are wired in, not vestigial' },
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'docker run --name nc-sso-test -p 10399:8080 (no NC_DB → SQLite)' },
+ { t: 'err', text: 'curl /auth/oidc → ### UNCAUGHT EXCEPTION ### exits(1)' },
+ { t: 'hl', text: 'one unauthenticated GET is enough to stop the instance' },
+ { t: 'dim', text: 'CE mode · meta=MySQL (licensing needs Postgres) · fork 0.255.2' },
+ { t: 'prompt', text: '$' }, { t: 'cmd', text: 'gate at the edge instead · prod container never touched' },
+ { t: 'ok', text: '→ OIDC SSO is Business+ · one sentence, not an afternoon ✓' },
+ ],
+ flow: [
+ { n: '1', label: 'NC_SSO vars', err: true },
+ { n: '2', label: 'throwaway box' },
+ { n: '3', label: '/auth/oidc dies' },
+ { n: '4', label: 'licence + Postgres' },
+ { n: '5', label: 'edge gate ✓' },
+ ],
+};
+
// ---------- read frontmatter ----------
const postPath = join(ROOT, 'src', 'content', 'posts', `${slug}.md`);
let category = 'devops';
diff --git a/scripts/og-gen/generate.mjs b/scripts/og-gen/generate.mjs
index 4586a27..99c3dfa 100644
--- a/scripts/og-gen/generate.mjs
+++ b/scripts/og-gen/generate.mjs
@@ -219,6 +219,24 @@ TERMINALS['adding-english-mode-to-a-chinese-only-web-app'] = `
the UI is 100% Chinese · browsers then never offer translate
$nginx sub_filter + gate-en.js · 871 zh→en labels→ 88% English ✓
`;
+TERMINALS['vetting-an-open-source-dependency-before-you-bet-on-it'] = `
+ $grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md→ 0 hits
+ the spec assumed OIDC · partner login was never implementable
+ $head -5 docs/white-label-custom-domains.md→ Target branch: multi-tenant
+ $selfhost mode · manual payouts · pin the commit→ 6 assumptions corrected ✓
`;
+
+TERMINALS['authentik-forward-auth-gate-wasnt-live'] = `
+ $curl -sk -H 'Host: app.example.com' https://localhost/→ 302 gated ✓
+ $curl -sI https://app.example.com/→ 200 x-powered-by: Express
+ the tunnel rule answered · nginx was never in the path
+ $verify from outside · read which layer replied→ gate real ✓
`;
+
+TERMINALS['nocodb-sso-is-a-licensed-feature'] = `
+ $NC_SSO=oidc · app boot→ env keys enforced
+ $curl -s localhost:10399/auth/oidc→ uncaught TypeError
+ unhandledRejection · container exits(1)
+ $CE mode · meta=MySQL · fork 0.255.2 (2024)→ gate at the edge ✓
`;
+
// ---------- read frontmatter ----------
const postPath = join(ROOT, 'src', 'content', 'posts', `${slug}.md`);
if (!existsSync(postPath)) {
diff --git a/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md b/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md
new file mode 100644
index 0000000..c61cdfc
--- /dev/null
+++ b/src/content/posts/vetting-an-open-source-dependency-before-you-bet-on-it.md
@@ -0,0 +1,177 @@
+---
+title: "How I Vet an Open-Source Dependency Before Betting On It"
+description: "Six checks that catch a repo which doesn't do what its docs claim — grep for the feature, read the doc's target branch, check the tier gate and the payment rail."
+pubDate: 2026-09-29
+category: devops
+tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"]
+ogImage: "/og/vetting-an-open-source-dependency-before-you-bet-on-it.png"
+banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png"
+draft: false
+---
+
+I was handed a 1,300-line architecture spec for a partner program. It read like a
+plan: consistent, cross-referenced, confident. It named a self-hosted
+open-source engine as the attribution and commission layer, and it assumed three
+things about that engine — that it spoke OIDC so my existing identity provider
+could log partners in, that its commission rules covered the five shapes the
+business needed, and that its white-label feature was shipped.
+
+Ten minutes of `curl` and `grep` later, two of those were false and the third was
+half-true. None of that is unusual. The README describes the *product*; the
+repository describes the *software*. The gap between those two is exactly where
+integration projects die — and it is cheap to measure before you write any code.
+
+## The problem, phrased as the question I'd actually search for
+
+**How do I know an open-source project does what its documentation claims before
+I build on it?**
+
+A spec is a claim *about other software*. The more polished the spec, the more its
+assumptions read like facts — this one cited sources, defined stable IDs, and had
+a source-of-truth matrix. What it never did was check whether the engine down the
+chain implemented the features being assigned to it. So before installing
+anything, I went looking for those specific features.
+
+## What I tried first, and why it wasn't enough
+
+**1. Read the README end to end.** It sells the pipeline well:
+`click → identity → event → attribution → commission → payout`. It never once
+says which authentication protocol the app speaks. For a feature you *need*,
+silence isn't neutral — but an absent mention is easy to skim past when the rest
+of the page is convincing.
+
+**2. Searched the web.** Several of my queries returned nothing usable. The
+project is a few months old and its SEO surface is thin. For software that's
+fine — the source *is* the documentation. It just means the reading has to happen
+in the repo, not in a blog post.
+
+**3. Found a 78 KB feature design doc and assumed the feature shipped.** It was
+labelled "FINAL, post-review", listed three reviewers, and carried a date. I took
+that as proof. That was the worst inference of the three, and the easiest to check.
+
+## The six checks that actually settled it
+
+### 1. Read the repo's vital signs before any feature claim
+
+```bash
+curl -s -H "User-Agent: hermes" https://api.github.com/repos// \
+ | grep -E '"created_at"|"pushed_at"|"stargazers_count"|"forks_count"|"open_issues_count"|"archived"|"spdx_id"'
+```
+
+What came back: MIT, created 2026-04-23, last push 2026-09-10, 8 stars, 3 forks,
+15 open issues, not archived. Read the same as: **real, still moving, and very
+small** — one vendor, which also sells a hosted tier. Now I know how much of my
+business I want resting on it *before* I start grading its features.
+
+### 2. Grep for the feature instead of reading for it
+
+```bash
+grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md | wc -l
+```
+
+Zero hits. The app authenticates with its own magic-link sessions and
+per-persona API keys; it does not speak OIDC at all. That one line of output
+killed a phase of the plan — "put the portal behind our SSO provider" — which
+would otherwise have surfaced weeks later, mid-integration, with a partner
+already invited. Reading for a feature you can't find is how you lose an
+afternoon; grepping makes the absence loud.
+
+### 3. Read the target branch line at the top of a feature doc
+
+```bash
+head -5 docs/white-label-custom-domains.md
+```
+
+`Target branch: multi-tenant`. The doc is a design for a branch that is not the
+default branch — it describes work in flight, so the feature is not in the code
+you would install. A finished-looking document with reviewers and dates can
+document something unshipped. That isn't dishonesty; it's what design docs are.
+
+### 4. Read the data model, not the feature list
+
+The commission section named exactly two rule types — `percent` (with a recurring
+flag) and `fixed`. The business spec had enumerated five. That gap isn't a bug;
+it's a modelling job I'd have to do in configuration instead of expecting from
+the product:
+
+| Spec assumed | What the engine actually has |
+|---|---|
+| first-order bonus | a `fixed` rule on the first event |
+| event-specific rules (`lead` vs `invoice_paid`) | one rule per campaign — so: two campaigns |
+| recurring commission | `percent` + `recurring: true` |
+| percentage | `percent` ✅ |
+| fixed | `fixed` ✅ |
+
+Two rule primitives, five behaviours — workable, but only if you design for it up
+front instead of discovering it during implementation.
+
+### 5. Check which paid tier gates the feature you need
+
+For the database tool in the same stack, the edition table was blunt:
+
+| Feature I needed | Community (free) | Enterprise (paid) |
+|---|---|---|
+| API tokens, so I can automate it | ✅ | ✅ |
+| SSO / OIDC | ❌ | ✅ |
+| Page designer, hide branding, 2FA | ❌ | ✅ |
+
+The thing I wanted for the partner-facing door (SSO) sits behind the commercial
+gate, while the thing I wanted for automation (an API token) is free. Knowing
+which side of the wall each requirement falls on changes the design — before
+you've designed around it.
+
+### 6. Check that the money rail works in your country
+
+The payout model was `stripe_connect | manual`, and self-hosted mode was
+documented as "operator manages out-of-band" — i.e. you move the money yourself.
+Stripe Connect is not how Malaysian businesses pay partners, so that one line
+decided the entire deployment shape: self-host mode, manual payouts, and event
+ingestion pushed from my own order system rather than leaning on the provider's
+payment integration. That line is never on a marketing page, and it mattered more
+than any feature.
+
+### Then: ask the fallback question before you need it
+
+"What if the feature I need isn't there?" For the missing SSO the answer was:
+don't put login in the app, put it in front of the app. authentik's proxy
+provider exists to protect "applications that do not support native
+authentication protocols such as OIDC, SAML, or LDAP", and it injects
+`X-authentik-username` and `X-authentik-groups` headers upstream, so the reverse
+proxy can do the gating. That turned "no SSO support" from a disqualification
+into an afternoon of config.
+
+## What I'd do differently
+
+- **Write the requirement list first, then go look for it.** Five lines — the
+ features *I* need — and search for those. Starting from the README's feature
+ list means grading the software on the vendor's exam.
+- **Treat "a doc exists" as different from "a feature ships".** Read the branch
+ line.
+- **Grep, don't read.** Presence is cheap to prove; absence is the thing you're
+ hunting.
+- **Decide the fallback before the decision**, so a missing feature becomes a
+ design change rather than a surprise.
+- **Pin the commit.** This project's README says its API is "stable but
+ unversioned" and there is no OpenAPI description anywhere in the tree. There's
+ no contract to compile against, so the commit hash *is* the contract — the
+ endpoints I use go into my own docs, and upgrades get a regression pass.
+
+## The result
+
+Six assumptions corrected in about ten minutes of `curl` and `grep`, before a
+single line of integration code existed. The project stayed viable — but
+reshaped: no OIDC, manual payouts, a pinned commit, and a plan that no longer
+depends on a feature sitting on an unmerged branch.
+
+The spec wasn't wrong to be optimistic. It was wrong to be unverified, and that's
+the cheapest thing on the whole project to fix.
+
+---
+
+**Building on a self-hosted stack, and want a second pair of eyes on whether
+those features will actually be there when you integrate?** That's the kind of
+review I do.
+
+[WhatsApp +60 12-797 2969](https://wa.me/60127972969) ·
+[me@hoelee.com](mailto:me@hoelee.com?subject=Open-source%20dependency%20review) ·
+[hoelee.com](https://hoelee.com)
diff --git a/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md b/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md
new file mode 100644
index 0000000..fdc3cfa
--- /dev/null
+++ b/src/content/posts/zh/vetting-an-open-source-dependency-before-you-bet-on-it.md
@@ -0,0 +1,113 @@
+---
+title: "如何在押注前验证一个开源依赖:我用的六项检查"
+description: "六项检查,专门抓出「文档说得漂亮、代码里其实没有」的仓库:用 grep 找功能、读文档头部标注的目标分支、看清付费档位门槛,以及收款通道在不在你的国家能用。"
+pubDate: 2026-09-29
+category: devops
+tags: ["open-source", "due-diligence", "oidc", "self-hosting", "docker"]
+ogImage: "/og/vetting-an-open-source-dependency-before-you-bet-on-it.png"
+banner: "/banners/vetting-an-open-source-dependency-before-you-bet-on-it.png"
+draft: false
+---
+
+有人给我一份 1306 行的伙伴系统架构规格书。读起来像一份计划:前后一致、交叉引用、语气笃定。它指定一个自托管的开源引擎作为归因与佣金层,并且假定了那个引擎的三件事——它支持 OIDC,所以我现有的身份提供方能登录伙伴;它的佣金规则覆盖业务需要的五种形态;它的白标功能已经上线。
+
+十分钟的 `curl` 加 `grep` 之后,三件里有两件是假的,第三件只对了一半。
+
+这没什么稀奇。README 描述的是**产品**,仓库描述的是**软件**。这两者之间的落差,正是集成项目死掉的地方——而且在写任何代码之前,测量这个落差很便宜。
+
+## 问题,写成我真的会去搜的那句话
+
+**我怎么知道一个开源项目真的做了它文档宣称的事,然后才在它上面开始建东西?**
+
+规格书是**关于别的软件**的一种主张。规格书越漂亮,它的假设就越像事实——这一份引用了来源、定义了稳定 ID、还画了一张「真源矩阵」。它唯一没做的事,就是核对链条下游那个引擎到底有没有实现被指派给它的功能。所以在安装任何东西之前,我去找那几个具体功能。
+
+## 我一开始试的,以及为什么不够
+
+**一、把 README 从头读到尾。** 它把流水线卖得很好:`click → identity → event → attribution → commission → payout`。但它一次都没说这个应用讲哪种认证协议。对一个你**必须要有**的功能来说,沉默不是中立——只是当页面其他部分都很有说服力时,一个「没被提到」很容易被眼睛滑过去。
+
+**二、上网搜。** 好几条查询都没返回能用的结果。这个项目才几个月大,SEO 面很薄。对软件来说这没关系——源码**就是**文档。只是意味着阅读必须在仓库里进行,而不是在博客文章里。
+
+**三、看到一份 78 KB 的功能设计文档,就假定功能已上线。** 它标着「FINAL, post-review」,列了三位评审,还带着日期。我把它当成了证据。这是三个推断里最糟的一个,也是最容易核对的一个。
+
+## 真正定案的六项检查
+
+### 1. 先看仓库的生命体征,再看任何功能宣称
+
+```bash
+curl -s -H "User-Agent: hermes" https://api.github.com/repos// \
+ | grep -E '"created_at"|"pushed_at"|"stargazers_count"|"forks_count"|"open_issues_count"|"archived"|"spdx_id"'
+```
+
+返回的是:MIT、创建于 2026-04-23、最后推送 2026-09-10、8 个 star、3 个 fork、15 个 open issue、未归档。翻译过来就是:**真实、还在动、但非常小**——单一厂商,而且它同时卖托管档。现在我在开始评它的功能之前,先知道了我愿意把多少生意压在它身上。
+
+### 2. 用 grep 去找功能,而不是用读的
+
+```bash
+grep -in 'oidc\|sso\|saml' README.md ARCHITECTURE.md docs/*.md | wc -l
+```
+
+零命中。这个应用用的是它自己的 magic-link 会话和按身份签发的 API key;它根本不讲 OIDC。就这一行输出,砍掉了计划里的一个阶段——「把门户挂到我们 SSO 后面」——否则这一步会在几周后才浮现,而且是在集成到一半、伙伴已经被邀请进来的时候。**用读的方式去找一个找不到的功能,就是你失去一个下午的方式;grep 让「不存在」这件事变得响亮。**
+
+### 3. 读功能文档最上方那行「目标分支」
+
+```bash
+head -5 docs/white-label-custom-domains.md
+```
+
+`Target branch: multi-tenant`。这份文档描述的是**另一个分支**上的设计,不是你默认会安装的那个分支——它写的是在途工作,所以功能不在你会装下来的代码里。一份有评审、有日期、看起来已经完成的设计文档,完全可能描述着尚未上线的东西。这不是不诚实,设计文档本来就是这个样子。
+
+### 4. 读数据模型,不要读功能清单
+
+佣金那一节只列了两种规则类型——`percent`(带续期标记)与 `fixed`。业务规格书列了五种。这个落差不是 bug,而是我必须在**配置层**做的建模工作,不能指望产品自带:
+
+| 规格书假设 | 引擎实际有的 |
+|---|---|
+| 首单奖励 | 对首个事件用一条 `fixed` 规则 |
+| 按事件区分(`lead` vs `invoice_paid`) | 一个 campaign 一条规则——所以:两个 campaign |
+| 续期佣金 | `percent` + `recurring: true` |
+| 百分比 | `percent` ✅ |
+| 固定金额 | `fixed` ✅ |
+
+两种规则原语,五种行为——能做,但前提是你**一开始**就照这个设计,而不是在实现途中才发现。
+
+### 5. 看清你需要的那项功能被锁在哪一档
+
+同一个技术栈里还有个数据库工具,它的版本对照表很直白:
+
+| 我需要的功能 | Community(免费) | Enterprise(付费) |
+|---|---|---|
+| API token,好让我自动化 | ✅ | ✅ |
+| SSO / OIDC | ❌ | ✅ |
+| 页面设计器、隐藏品牌、双因素 | ❌ | ✅ |
+
+我想给「伙伴看到的那个门」用的东西(SSO)在商业门后面;而我想用来做自动化的东西(API token)是免费的。在围绕它做设计**之前**,先弄清每项需求落在墙的哪一侧。
+
+### 6. 核对收款通道在你的国家能不能用
+
+它的打款模型是 `stripe_connect | manual`,而自托管模式被写成「operator manages out-of-band」——也就是钱由你自己搬。Stripe Connect 不是马来西亚企业付钱给伙伴的方式,所以就是这一行决定了整个部署形态:自托管模式、手工打款、事件由我自己的订单系统推送过去,而不是依赖它内置的支付集成。这一行从来不会出现在营销页上,而它比任何功能都重要。
+
+### 然后:在你需要之前,先问「兜底方案」
+
+「如果我需要的功能不在,怎么办?」针对缺失的 SSO,答案是:不要把登录做进应用里,把登录放到应用**前面**。authentik 的 proxy provider 存在的意义就是保护「不支持 OIDC、SAML 或 LDAP 等原生认证协议的应用」,并且向上游注入 `X-authentik-username` 和 `X-authentik-groups` 头,于是反向代理就能承担准入判断。这就把「不支持 SSO」从一条否决理由,变成了一个下午的配置工作。
+
+## 如果重来,我会怎么做
+
+- **先写需求清单,再去找它。** 五行——**我**需要的功能——然后就搜这几个。从 README 的功能列表出发,等于用厂商的考卷给软件打分。
+- **把「文档存在」和「功能上线」当成两件不同的事。** 去读那行目标分支。
+- **用 grep,不要用读的。** 证明「有」很便宜;你要猎的是「没有」。
+- **在决定之前先定兜底**,这样一个缺失的功能就只是一次设计调整,而不是一场意外。
+- **把 commit 钉住。** 这个项目的 README 自己写着 API「stable but unversioned」,而且仓库里没有任何 OpenAPI 描述。没有可以编译期对齐的契约,那么 commit hash **就是**契约——我用到的端点要记进我自己的文档,升级前跑一遍回归。
+
+## 结果
+
+六个假设在大概十分钟的 `curl` 和 `grep` 里被修正,而当时一行集成代码都还没写。项目依然可行——只是换了形状:没有 OIDC、手工打款、钉住 commit,以及一份不再依赖某个未合并分支上功能的计划。
+
+那份规格书乐观并没有错。它错在**没被验证过**——而这是整个项目里最便宜的修补项。
+
+---
+
+**你正在自托管技术栈上建东西,想在集成之前找人看一眼那些功能到底在不在?** 这正是我在做的复核工作。
+
+[WhatsApp +60 12-797 2969](https://wa.me/60127972969) ·
+[me@hoelee.com](mailto:me@hoelee.com?subject=Open-source%20dependency%20review) ·
+[hoelee.com](https://hoelee.com)