Maintained for Fourteen Months, Documented for Two
A Christmas Day commit on an unfinished feature, three years of unactioned dependabot, a final fix by a stranger - and the estate's real lesson, which is about re-reading rather than writing.
The README is written in the present tense. “The aim of this project is to provide an easily configurable and extendable API Gateway.” Nothing anywhere says the last human commit landed in 2019, that automated dependency updates arrived for three years afterwards and were never merged, or that the configuration reference on the front page describes property names the code stopped using six weeks after it was written.
Part 10 closed the operational tier. This part closes the ledger on eleven parts of estate reading, and the honest verdict turns out to be about neither the code nor the documentation individually, but about the gap between the two clocks they run on.
Two clocks
The code was maintained for fourteen months. 8711479 “init”, 6 November 2018, when the project was still called NGate. 44856a6 “Improved swagger endpoints”, 25 December 2019. One hundred and one of the one hundred and three commits are by one person under three git identities. Bus factor: one.
The documentation that exists today was maintained for two. The README's “Advanced configuration” block — 124 of its 225 lines, and the only configuration reference this project has ever had — was rewritten wholesale by c9d6bcb on 28 October 2019, extended once by fff7ebb on 10 November, and then received exactly one line of change ever: a docker run port mapping in 5016fdb on 20 December. Six weeks of active life, two months from first shape to last touch, inside a fourteen-month project.
And the two months are the last two, which means everything the README says about the first twelve was a reconstruction the moment it was written. c9d6bcb proves it. That commit introduced the extensions: block — the structure produced by the September 2019 plugin extraction — and left useErrorHandler: true and useJaeger: true sitting four lines above it. Both keys had been dead for five weeks by then; the options they configure had moved into extensions: customErrors: and extensions: tracing:, and NtradaOptions has no property for either. The signature of an additive edit: new keys pasted in, old keys never removed. The document that is supposed to explain the configuration language was already wrong about it on the day it was authored.
Then, on 20 December, the author edited the README at 20:37 and renamed the JWT extension's options properties at 20:47 — the story part 7 tells in full. Ten minutes between the last documentation change of the project's life and the commit that invalidated it, by the same person, in the same session. Nobody came back to either.
The drift, in one table
| Documented | Reality |
|---|---|
useErrorHandler, useJaeger |
No such properties; both moved under extensions: in September 2019, and both still appear beside their replacements in the same file |
auth: claims: |
Configuration.Auth has Enabled, Global, Policies. Claims are per-route and per-policy, never global |
Quick-start modules: as a list |
NtradaOptions.Modules is a dictionary. The first five lines of YAML a reader sees cannot bind |
Quick-start return_value: |
The property binds returnValue: — a snake_case handler key beside a camelCase property |
jwt: key: / issuer: / audiences: |
Four dead keys, three restating defaults, one with effect |
| “Static content” in the feature list | No implementation anywhere; the static-files package left the core in September 2019 |
modulesPath, payloadsFolder |
Both bind correctly. Neither directory exists in the repository |
| Extensions: five listed | Six exist. Swagger is undocumented and absent from the Docker host |
loadBalancer: enabled / url |
Real, and it is URL rewriting. One URL, no pool, no algorithm |
Every single one of those drifts is silent. IConfiguration.Bind discards unmatched keys without comment, by design. There is no options validation, no unknown-key detection, no schema. And the estate ships NJsonSchema, which it uses to validate users' request payloads against declared schemas. The one document this gateway never validates is its own configuration file.
The mechanism behind the whole table is legible and it is worth naming precisely. Compare README.md:103-225 with samples\Ntrada.Samples.Api\ntrada.yml:1-134: they differ only in the sample's swagger: block, its excludePaths list, and a commented-out # method: GET. The README's “Advanced configuration” is not documentation. It is a paste of a config file, taken once, and never re-synced — out of date with the sample it was copied from, and with the code the sample was already out of date with. Two generations of drift in one code fence. Documentation that is a snapshot of an artefact rather than a description of a contract decays at the artefact's rate, and nothing can verify it.
The counterweight is real and it is on line 45: "No documentation yet, please take a look at the basic ntrada.yml configuration." An acknowledged gap is categorically different from an unacknowledged one, and that sentence is honest. But it was written early and carried unchanged through eleven README commits over fourteen months to become the project's permanent final state — the admission stopped being a promissory note and became the documentation. And it loses on page weight: a 124-line YAML block titled “Advanced configuration” reads as a reference regardless of a disclaimer fifty-five lines above it.
What this estate gets right
Eleven parts of criticism earns an equally specific list on the other side, and this one is not a courtesy.
The extension contract is better plugin design than a lot of production frameworks of its era shipped. Four members, no ceremony, YAML-keyed identity, options bound by convention, no registration call — and six independently-publishable integration packages on the far side of it, the smallest under sixty lines of C#. Part 2 took it apart and the parts hold up.
AuthenticationManagerTests is genuinely excellent pedagogy. A complete decision matrix over a three-level authorisation cascade, each fact pairing a return-value assertion with an interaction assertion, in a naming convention you can read aloud. If the goal was to show a student what a good unit test looks like, it succeeded completely.
The .rest file is a smart executable teaching artefact. Thirty-three lines capturing a behaviour described in prose nowhere else: the gateway generates a resource id on POST, returns it in a Resource-ID header, and the collection feeds it into the follow-up GET and DELETE. An onboarding artefact doing load-bearing documentation work the README does not know it is doing.
The archival branches show real consideration. origin/netcoreapp2.2 and origin/netcoreapp3.0 sit at the last commit before each migration, zero commits ahead, preserved for readers stuck on older runtimes. The best estate hygiene in the repository, and it cost two git push commands.
Deleting the user-id feature was right, and deleting it fast was righter — four days from “the broker rejects this” to “it is not in the codebase,” as part 6 traces.
And three quieter ones: route matching is delegated wholesale to ASP.NET Core's endpoint routing rather than reimplemented; IHttpClientFactory is used correctly, which is the entire reason downstream calls are traced end to end for free; and thirty implementation types are internal sealed, so a consumer cannot accidentally depend on anything the maintainer might want to rewrite.
How it ended
The last three commits are Swagger: 888b20f “Swagger preview” on 22 December, ea04c96 on the 24th, 44856a6 “Improved swagger endpoints” on Christmas Day. The extension they produced ships with mixed rc4/rc5 pre-release packages, no LangVersion, a configuration sample misnamed rabbitmq.yml that contains swagger: options, no mention in the README, and no reference from the Docker host — so it cannot be enabled in the container at all. The project's final act by its author is an unfinished feature committed on Christmas Day, and then nothing. origin/develop points at 44856a6, master minus one: the gitflow branch abandoned mid-flow.
The last commit of any kind is 84f0882, 8 November 2020 — 318 days later, from an external contributor, fixing a missing Content-Type on response headers. The same class of bug the author had already fixed five times, in the same 443-line untested handler, and it ran no CI at all, because on: [push] does not fire for pull requests from forks.
Then the long tail. Two dependabot branches sit unmerged on the remote: a JwtBearer bump from 25 August 2021 and a Swashbuckle bump from 6 January 2023. Automated maintenance kept arriving for three years after the last human commit, on branches the pipeline could never have run, so there was never even a green check to act on. The repository outlived its maintainer's attention by longer than it was maintained. There are no tags, no releases, no CHANGELOG, and no archived notice of any kind.
The stop is not a decay curve — it is a cliff. The last ten weeks are as productive as any ten weeks in the project's life. The author did not run out of energy; the centre of gravity moved. The tell is in the README's own outbound link, which points readers wanting real-world examples at Pacco — under a different GitHub organisation than the one that owns this repository. By late 2019 Ntrada had become a consumed dependency in somebody else's reference architecture rather than a maintained product, and consumed dependencies do not get README updates.
When to use it, and what I would change
Use Ntrada when you want declarative routing in front of a .NET service estate that already has an ingress doing TLS, rate limiting and health gating; when your routes change rarely enough that a restart is acceptable; and when the traffic is JSON. It is a very good declarative router, and the config vocabulary was designed with genuine care.
Do not use it when you need to proxy anything that is not text, when you need resilience at the edge rather than behind it, when you need configuration injected into a container without a rebuild, or when a 202 has to mean something.
Two changes would have prevented most of eleven articles, and neither is a rewrite. One Directory.Build.props — ten lines centralising target framework, language version, package version, authors, licence expression and repository URL across twelve projects — makes part 8 structurally impossible and half of part 9 visible. One unknown-key check on the configuration bind — even just a log line naming YAML keys that matched no property — turns the entire drift table above into a startup warning that anybody would have fixed in an afternoon.
What both series were actually about
The companion series, No Coding Whatsoever, read the gateway's own source and closed on Nothing Was Ever Frozen — configuration that stays mutable, ordering that stays emergent, one method absorbing every concern. This one read everything around that source. They arrive at the same place from opposite directions, and the sentence they meet at is this:
Ntrada is a teaching repository whose teaching artefacts were built once, with real care, and then never re-read.
That distinction matters, because the estate is full of things that are not teaching simplifications. A simplification removes complexity and leaves the lesson intact — omitting rate limiting from a gateway that demonstrates routing is a legitimate scope decision, honestly made. But a retry formula that inverts base and exponent, a quick-start snippet that cannot parse, a signing key that does not bind, a pipeline requesting an SDK two majors stale, a licence property that is not a property, a container that cannot be configured: none of those teaches anything. Each one is what a defect looks like when nobody is reading the artefact it lives in.
Which is the thing I would take from four weeks inside this repository. Writing the README, the sample, the .rest file and the test suite was the easy part, and the author did all four well. Re-reading them — at all, once, after any refactor that touched the contract they describe — is the part that never happened, and it is the part that costs almost nothing and decides everything. If you maintain anything with a sample config in it, the question is not whether your documentation was good when you wrote it. It is when you last opened it.