Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
/backend/internal/system/jose/ @hwupathum


## architectural decision records

/docs/adrs/ @darshanasbg @madurangasiriwardena


## scripts

/backend/dbscripts/ @rajithacharith @darshanasbg
Expand Down
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ _How should users update their code/configuration to adapt to the breaking chang
### Related PRs
- N/A

### Related ADRs
<!-- Link the architectural decision record this change implements or is constrained by, e.g. ADR-0007.
See https://github.com/thunder-id/thunderid/blob/main/docs/adrs/README.md for when a record is required. -->
- N/A

### Checklist
- [ ] Followed the contribution guidelines.
- [ ] Checked whether this change requires an [ADR](https://github.com/thunder-id/thunderid/blob/main/docs/adrs/README.md#when-an-adr-is-required); if so, it is linked under Related ADRs.
- [ ] Manual test round performed and verified.
- [ ] Documentation provided. (Add links if there are any)
- [ ] Ran Vale and fixed all errors and warnings
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/adr-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: 📐 ADR Lint

on:
pull_request:
paths:
- 'docs/adrs/**'
merge_group:
workflow_dispatch:

permissions:
contents: read

jobs:
adr-checks:
name: 📐 Validate Decision Records
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 📥 Checkout Code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

# The root package.json pins pnpm through `devEngines`, which makes npm
# refuse to run `npx` anywhere in this repository.
- name: ⚙️ Set up Node.js and pnpm
uses: ./.github/actions/setup-pnpm
with:
cache-store: 'false'

- name: 🧹 Run markdownlint
run: >
pnpm dlx markdownlint-cli2@0.14.0
--config docs/adrs/.markdownlint.yml
"docs/adrs/*.md"

- name: 🔍 Check ADR Conventions
run: |
set -euo pipefail

shopt -s nullglob
records=(docs/adrs/[0-9]*.md)
shopt -u nullglob

if [ ${#records[@]} -eq 0 ]; then
echo "No decision records found."
exit 0
fi

fail=0

for f in "${records[@]}"; do
base=$(basename "$f")

# Filename must be NNNN-lowercase-with-dashes.md
if ! [[ "$base" =~ ^[0-9]{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$ ]]; then
echo "::error file=$f::filename must match NNNN-title-with-dashes.md"
fail=1
fi

# A status field must be present and hold a recognised value.
status=$(awk '/^---$/{n++; next} n==1 && /^status:/{print; exit}' "$f" || true)
if [ -z "$status" ]; then
echo "::error file=$f::missing status in YAML front matter"
fail=1
elif ! echo "$status" | grep -Eq '^status: *"(proposed|rejected|accepted|deprecated|superseded by ADR-[0-9]{4})"'; then
echo "::error file=$f::unrecognised status value: $status"
fail=1
fi

# Every record must appear in the index table, and the status column
# must agree with the record's front matter.
# Restricted to table rows: a passing mention in the README prose is
# not an index entry.
row=$(awk -v needle="($base)" \
'substr($0, 1, 1) == "|" && index($0, needle)' docs/adrs/README.md)
if [ -z "$row" ]; then
echo "::error file=$f::not listed in docs/adrs/README.md index"
fail=1
elif [ -n "$status" ]; then
declared=$(echo "$status" | sed -E 's/^status:[[:space:]]*"?//; s/"?[[:space:]]*$//')
indexed=$(echo "$row" | awk -F'|' '{gsub(/^[[:space:]]+|[[:space:]]+$/, "", $4); print $4}')
if [ "$declared" != "$indexed" ]; then
echo "::error file=$f::index status \"$indexed\" does not match front matter status \"$declared\""
fail=1
fi
fi
done

# Numbers must be unique.
dupes=$(printf '%s\n' "${records[@]}" \
| xargs -n1 basename \
| cut -c1-4 | sort | uniq -d)
if [ -n "$dupes" ]; then
echo "::error::duplicate ADR numbers: $dupes"
fail=1
fi

exit $fail
1 change: 1 addition & 0 deletions .github/workflows/pr-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
filters: |
docs:
- 'docs/**'
- '!docs/adrs/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
Expand Down
3 changes: 3 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Vale.PassiveVoice = NO
Vale.Readability = NO
TokenIgnores = (<[A-Z][^>]*>),(</[A-Z][^>]*>),(<(?!\!--)[^>]+>),(\!\[[^\]]*\]\([^)]+\)),(\[[^\]]*\]\([^)]+\)),(\[[^\]]*\]\[[^\]]+\]),(\[[^\]]+\]:\s+\S+),(@[a-zA-Z0-9_-]+\/[a-zA-Z0-9._/-]+),(\{`[^`]*`\})

[**/adrs/*.md]
BasedOnStyles =

[**/SKILL.md]
BasedOnStyles =

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Load only the guidance the task needs:
| Backend Go code | [backend/AGENTS.md](backend/AGENTS.md) |
| Frontend React code | [frontend/AGENTS.md](frontend/AGENTS.md) |
| Documentation | [docs/AGENTS.md](docs/AGENTS.md) |
| A decision that needs an architectural record | [docs/adrs/README.md](docs/adrs/README.md) |
| Database schema, queries, or stores | [.agent/skills/db/SKILL.md](.agent/skills/db/SKILL.md) |
| Browser automation / Console UI verification | [.agent/skills/console/SKILL.md](.agent/skills/console/SKILL.md) |

Expand Down
22 changes: 22 additions & 0 deletions docs/adrs/.markdownlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# markdownlint configuration for architectural decision records.
# Based on the configuration shipped with MADR 4.0.0.

# MD013/line-length: disabled. Records use one sentence per line, so lines vary
# in length by design and wrapping them would make diffs harder to review.
MD013: false

# MD024/no-duplicate-heading: allow the same heading text under different parents.
# "Consequences" appears under Decision Outcome and option headings repeat under
# "Pros and Cons of the Options".
MD024:
siblings_only: true

# MD004/ul-style: MADR uses asterisks as list markers.
MD004:
style: asterisk

# MD025/single-title: one H1 per file, which is the record title.
MD025: true

# MD034/no-bare-urls: keep URLs in angle brackets or link syntax.
MD034: true
113 changes: 113 additions & 0 deletions docs/adrs/0000-use-madr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
status: "accepted"
date: "2026-09-01"
decision-makers: "@madurangasiriwardena @darshanasbg"
---

# Use MADR for architectural decision records

## Context and Problem Statement

ThunderID has no decision log, in the sense of a maintained collection of records that each capture one architectural decision (AD) and its rationale.
Design reasoning currently lives in GitHub Discussions, pull request threads, and, for decisions inherited from earlier work, internal mail threads that external contributors cannot read at all.

This has three costs that grow as the project does.
Contributors cannot tell whether an approach was already considered and rejected, so the same proposals resurface.
Decisions that were deliberate deviations from a specification are indistinguishable from oversights, which matters disproportionately for software in the identity domain.
And the reasoning behind a design is reconstructed from a thread whose conclusion is often implicit, if it survives at all.

The project is governed under the Open Wallet Foundation, and external contributors build production systems on it.
Those contributors need the same access to design rationale that maintainers have.

How should decisions that address an architecturally significant requirement (ASR) be recorded, so that the reasoning survives the thread it was formed in?

## Decision Drivers

* Records must be readable and writable by external contributors with no access to internal systems.
* The format must add little enough friction that it is actually used under delivery pressure.
* Records must be reviewable through the mechanism the project already uses for changes: pull requests.
* Decision history must be versioned alongside the code it describes.
* The scheme must survive the project outliving any individual maintainer.

## Considered Options

* Continue with GitHub Discussions only
* Michael Nygard's original ADR format
* MADR 4.0.0
* Y-statements
* A wiki or Confluence space

## Decision Outcome

Chosen option: "MADR 4.0.0", because it is the only option that keeps records in the repository under normal pull request review, offers enough structure to make rejected options explicit, and remains lean enough to write in one sitting.

### Consequences

* Good, because decision history is versioned with the code and travels with any fork or clone of the repository.
* Good, because proposing a decision uses the same pull request mechanism external contributors already use, giving them a legitimate path to influence architecture rather than only implementation.
* Good, because the log is complete with respect to the code in this repository: a decision affecting code here is recorded here, so a reader of the repository is never missing rationale that exists elsewhere.
* Good, because the structure forces rejected options to be written down, which is the part reconstruction from a thread always loses.
* Neutral, because MADR is a template rather than a tool; no tooling dependency is added, and none is provided either.
* Bad, because writing a record is real work that competes with delivery, and the practice will decay unless the trigger conditions and review integration in `README.md` are enforced.
* Bad, because a partially adopted log is arguably worse than none. Readers may reasonably infer that an undocumented decision was never deliberately made.

### Confirmation

Adoption is enforced by three mechanisms rather than by intent:

* A `CODEOWNERS` entry on `docs/adrs/` requiring architectural review on every record.
* A checkbox in the pull request template asking whether the change requires an ADR.
* An `adr-lint` workflow that fails on malformed records and on records missing from the index.

Whether the log is actually being *read* is confirmed by a softer signal: architectural objections in code review citing ADR numbers.
If that has not started happening within two release cycles, this decision should be revisited rather than quietly ignored.

## Pros and Cons of the Options

### Continue with GitHub Discussions only

* Good, because it costs nothing and is already in use.
* Good, because it captures the full debate, including the arguments that were abandoned.
* Neutral, because discussions are public and therefore already accessible to external contributors.
* Bad, because a discussion has no canonical conclusion. The outcome is implicit in the last few comments, or absent.
* Bad, because there is no status, so a superseded decision looks identical to a current one.
* Bad, because discussions are not versioned with the code and do not travel with a fork.

### Michael Nygard's original ADR format

* Good, because it is the most widely recognized ADR format and the origin of the term.
* Good, because it is extremely short: context, decision, status, consequences.
* Bad, because it has no dedicated section for options considered, which is the section that most often prevents a settled question from being reopened.
* Neutral, because MADR is a direct descendant and the two are close enough that migration in either direction is mechanical.

### MADR 4.0.0

* Good, because only four sections are mandatory, so a minor record can be fifteen lines.
* Good, because "Pros and Cons of the Options" makes the comparison explicit, with Good, Neutral and Bad prefixes.
* Good, because the "Confirmation" subsection asks how compliance will be verified, which maps directly onto conformance suites for protocol decisions.
* Good, because it ships markdownlint configuration and templates in four levels of verbosity.
* Neutral, because the format has changed across major versions, with section names differing between 3.x and 4.x, so records copied from other projects may need adjusting.
* Bad, because the fuller template is verbose enough that contributors may skip it entirely rather than delete the optional sections; the minimal template variants mitigate this.

### Y-statements

* Good, because a decision compresses to a single structured sentence, making it very cheap to write.
* Bad, because that compression discards the option comparison and the consequences, which is most of the value for a long-lived project.
* Neutral, because a Y-statement works well as a summary line *inside* a MADR record.

### A wiki or Confluence space

* Good, because it is easy to write in and easy to reorganize.
* Bad, because pages are mutable by default, so there is no reliable history of what was decided when.
* Bad, because it is not reviewable through pull requests.
* Bad, because access for external contributors is an administrative problem that recurs with every new contributor, and is fundamentally at odds with an Open Wallet Foundation project.

## More Information

* Architectural decision terminology, including AD and ASR: <https://adr.github.io/>
* MADR project: <https://adr.github.io/madr/>
* MADR 4.0.0 release: <https://github.com/adr/madr/releases/tag/4.0.0>
* Nygard's original article: <https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions>
* Trigger conditions, process, and repository integration: [README.md](README.md)

This decision should be revisited if the log falls out of use, or if the project adopts documentation tooling that makes a different format cheaper to maintain.
Loading
Loading