GitRoot

Craft your forge, Build your project, Grow your community freely
  1---
  2title: Issues, Tickets, Discussions... Call them what you want!
  3og:title: Issues, Tickets, Discussions... Call them what you want!
  4og:description: No database, no vendor lock-in, just pure Git. Discover how GitRoot reimagines issues, boards, and TODOs using only Markdown files and smart plugins.
  5description: No database, no vendor lock-in, just pure Git. Discover how GitRoot reimagines issues, boards, and TODOs using only Markdown files and smart plugins.
  6og:image: https://gitroot.dev/branches/issueBlog/blog/issues-cover.png
  7date: 2026/06/08
  8emoji: ✨
  9---
 10
 11# Issues, Tickets, Discussions... Call them what you want!
 12
 13Many of you, when checking out [https://gitroot.dev](https://gitroot.dev), have told me that the `🐞 Issues` button shouldn't be labeled "Issues", but rather "Tickets" or "Discussions".
 14
 15First of all, thanks for the feedback! I really appreciate it, that's how I can build a better forge. Of course, I will change the label, but developing a forge means having a lot of different areas to work on: issues/boards, git, code, CI/CD, and with GitRoot, even more, like plugins and WASM...
 16
 17So, why don't I just change this label right away? In reality, this label isn't hardcoded inside GitRoot. It's only there because I configured plugins to display it. I want to take this opportunity to explain how GitRoot works, because the platform is developed around this exact concept: how to customize a forge at the project level.
 18
 19## The Original Idea
 20
 21Imagine you are developing a forge, and to avoid vendor lock-in, you decide not to put anything outside of Git. You choose not to use any database at all. In this context, how do you design an "issue" system?
 22
 23In traditional forges, an "issue" consists of a title and a description created by a user. We can represent it like this:
 24
 25```mermaid
 26classDiagram
 27    class Issue{
 28        int id
 29        string title
 30        string description
 31    }
 32    class User{
 33        int id
 34        string email
 35    }
 36    class Status{
 37        <<enumeration>>
 38        Open
 39        TODO
 40        DOING
 41        CLOSE
 42    }
 43    Issue -- User : Author
 44    Issue -- Status : State
 45```
 46
 47Easy, right? Well, how do you represent that in GitRoot using only files? This question was running through my mind for months before I finally found a solution.
 48
 49## The Basic Solution
 50
 51Since I only have files at my disposal, what actually constitutes an "issue"? I need to add some metadata to a file so the system understands that this file is an issue and not, say, a `main.go`. I’ll probably want to group all these "issues" into a specific directory, because while GitRoot technically lets you do whatever you want (even dropping an issue right in the middle of your source code), I personally prefer to keep my files organized!
 52
 53A simple text format that handles metadata perfectly is Markdown. I use Markdown here, but any other markup language could work. Let's create a file named `issues/1.md`:
 54
 55```markdown
 56---
 57id: 1
 58user: email@email.com
 59status: OPEN
 60---
 61
 62# My title
 63
 64My description where I can detail what my problem is.
 65```
 66
 67Yes, it really is that simple. In GitRoot, to create an issue, you just create a file, add some metadata, commit, and push. Boom, you have an issue.
 68
 69Now, you might be thinking: "Well, I lose a lot of control with that, the status could be anything!" or "How are other users supposed to know what to put where?". You're right, and that is exactly where plugins enter the game.
 70
 71## A Better Solution
 72
 73If I simplify this as much as possible, what does an external contributor actually need to provide to create an issue? Just a title and a description. So, let's create a file named `issues/my_issue.md`:
 74
 75```markdown
 76# My title
 77
 78My description where I can detail what my problem is.
 79```
 80
 81Then, we run `git commit && git push`. Since I have configured the [ladybug plugin](../plugins/name/ladybug.md) like this:
 82
 83```yaml
 84- name: ladybug
 85  ...
 86  - path: issues/**/*.md
 87    branch:
 88    - "*"
 89    when:
 90    - add
 91    - mod
 92    func: []
 93    write:
 94      git:
 95      - path: issues/**/*.md
 96        can:
 97        - mod
 98      web: []
 99      exec: []
100      callfunc: []
101    configuration:
102      metadata:
103      - default: autogenerated
104        mandatory: true
105        name: id
106        type: crc16
107      - default: OPEN
108        mandatory: true
109        name: status
110        type: string
111      - default: email@email.com
112        mandatory: false
113        name: user
114        type: string
115```
116
117> You can read this configuration as: "For every Markdown file in the `issues` directory across all branches, if the file doesn't have these metadata fields, automatically add these default values."
118
119Now, if you are a Git purist, a server creating commits right on top of yours might sound a bit radical, or even terrifying. But look closely at the `write` block in the YAML configuration above:
120
121```yaml
122write:
123  git:
124    - path: issues/**/*.md
125      can:
126        - mod
127```
128
129GitRoot doesn't just let plugins run wild, the project configuration must explicitly grant the plugin permission to write or modify files on specific paths. If you don't declare these rights, GitRoot will block the plugin from making any changes.
130
131When granted, GitRoot intercepts your push, lets the plugin inject the missing metadata, and safely generates a follow-up commit. And don't worry about your history: if you decide to `git rebase -i` later to squash or fixup your commits, plugins won't generate duplicate commits if no metadata has changed.
132
133What about security or external users trying to forge metadata? Since contributors cannot push directly to the [default branch](../doc/technicals/default_branch.md) (like `main`), they have to go through a branch and need to be merged. This means every single change to an issue's status or metadata is subjected to a code review.
134
135If an external user tries to close someone else's issue or manipulate IDs, it will stand out like a sore thumb in the git diff. Maintainers retain absolute control and can simply reject the rogue contribution. Security doesn't rely on complex database permission tables, but on the battle-tested git review workflow you already know and trust.
136
137So, once you push, the server-side plugin does its job, and the next time you run `git pull`, your local file will automatically look like this:
138
139```markdown
140---
141id: 1
142user: email@email.com
143status: OPEN
144---
145
146# My title
147
148My description where I can detail what my problem is.
149```
150
151Bingo! You get the exact same file, with the exact same metadata you wanted. Of course, a lot more needs to be added, like defining a `type: user` to assign an issue, or perhaps a `type: status` to define an enum of allowed values. But I think you see where I'm going with this feature!
152
153_(Note: If you're wondering about the barrier to entry for non-technical contributors, my long-term vision is to allow plugins to intercept standard HTTP requests. Tomorrow, a classic web form will be able to POST to GitRoot and automatically commit a `.md` file under the hood. But for now, we are proudly dev-centric!)_
154
155## So, What About the Label?
156
157Throughout all of this, you’ll notice that the term "issue" only appears within the plugins themselves. Nowhere have we talked about that `🐞 Issues` button yet. We actually need to introduce another plugin before we can tackle that topic.
158
159First, we need a file that lists all of these "issues". That’s exactly why the [silo plugin](../plugins/name/silo.md) exists. It inspects all files and groups them whenever it finds a matching pattern.
160
161To list all open "issues", we configure it like this:
162
163```yaml
164- name: silo
165  ...
166  run:
167  - path: "**/*"
168    branch:
169    - main
170    when:
171    - add
172    - mod
173    - del
174    func: []
175    write:
176      git:
177      - path: boards/*.md
178        can:
179        - add
180        - mod
181        - del
182        - append
183      web: []
184      exec: []
185      callfunc: []
186    configuration:
187      boards:
188        description: All open issues
189        for: issues/*.md
190        format: table
191        paginator: 50
192        selects:
193        - "status: (.*)"
194        - "user: (.*)"
195        sort: select[0]
196        sortOrder: desc
197        tableHeader: "| | status | user |"
198        title: All issues
199        to: boards/issues.md
200        where: "status: OPEN"
201```
202
203> You can read this configuration as: "For every file in the main branch, create a board called 'All issues' containing links to all files matching `status: OPEN`. Display them as a table with `status` and `user` columns."
204
205After running `git commit && git push && git pull`, you will get a new file at `boards/issues.md` containing something like this:
206
207```markdown
208# All issues
209
210All open issues
211
212|                                   | status | user            |
213| --------------------------------- | ------ | --------------- |
214| [My title](../issues/my_issue.md) | OPEN   | email@email.com |
215```
216
217Silo has aggregated all your open issues into a single dashboard. Of course, you could easily create another board for closed issues in a separate file like `boards/closed.md`, it's entirely up to you.
218
219_(Note: If you are sweating at the thought of a plugin scanning your entire repository on every single push, don't panic. `silo`, thanks to the plugin-sdk API, doesn't do a full disk scan. It only processes the incremental diffs generated by the incoming commit. This is precisely why we restrict its scope to the `main` branch in this example.)_
220
221Now that we have a page representing all our issues, we can finally render it in the browser. Because, as you might have guessed, the label causing all this debate lives in the Web UI.
222
223## The Web is Where It Happens
224
225To render my `boards/issues.md` file on the web, I use the [apex](../plugins/name/apex.md) plugin, in conjunction with the [apex_markdown](../plugins/name/apex_markdown.md) plugin, of course.
226
227These plugins take files from git and render them directly in the Web UI. I configure them like this:
228
229```yaml
230- name: apex
231  ...
232  run:
233  - path: "**/*"
234    branch:
235    - main
236    when:
237    - add
238    - mod
239    - del
240    func: []
241    write:
242      git:
243      - path: "**/*"
244        can:
245        - add
246        - mod
247        - del
248        - append
249      web:
250      - path: "**/*"
251        can:
252        - add
253        - mod
254        - del
255        - append
256      exec: []
257      callfunc:
258      - pluginname: apex_markdown
259        funcname: renderMd
260      ...
261    configuration:
262      ...
263      menu:
264      - display: 🏠 Home
265        link: /
266      - display: πŸ“– Documentation
267        link: /doc/
268      - display: πŸ”– Versions
269        link: /CHANGELOG.html
270      - display: 🧩 Plugins
271        link: /plugins/
272      - display: 🐞 Issues
273        link: /boards/issues.html
274      - display: πŸš€ Code
275        link: /worktree/
276      - display: β˜• Blog
277        link: /blog/
278      - display: πŸ—¨ Contact
279        link: /contact.html
280```
281
282Oh, do you see it now? Yes, right there: `display: 🐞 Issues`. This is the exact configuration that tells Apex to add a global menu item with the `🐞 Issues` label, pointing to the `/boards/issues.html` file.
283
284If you are wondering how Apex magically knows where to find your "issues", the secret is: it doesn't.
285
286Apex isn't a smart router, it's just a file translator. It blindly takes the Markdown structure generated by GitRoot and renders it into HTML. Because I know Apex transforms `boards/issues.md` into `boards/issues.html`, I simply hardcode that target URL in my menu configuration (`link: /boards/issues.html`).
287
288And what about the links inside the table? Apex has a clever built-in rewrite engine. When it parses your generated board file, it automatically rewrites every internal Markdown link, turning `[My title](../issues/my_issue.md)` into a standard web-friendly `<a href="../issues/my_issue.html">`. No database, no dynamic routing, just plain old static files mapped one-to-one.
289
290## Conclusion
291
292If you change the label to `🐞 Tickets` or `πŸ’‘ Discussions`, the "issue to rename issues into something else" can finally be closed. Ideally, you might also want to rename some directories in the other plugins' configurations, but it's really not a big deal.
293
294I'm pretty sure you now understand why I didn't immediately jump on resolving this task: everything we just saw is pure configuration, completely separated from the GitRoot core code.
295
296And to be perfectly honest, I haven't changed it yet because none of the traditional options really appeal to me. I don't particularly like "Tickets" or "Discussions", maybe it should be "Feature Requests" instead (and yes, you can now model that in GitRoot too!).
297
298In fact, I personally call them `TODO`s, because in reality, they are all just waiting for me to get them done.
299
300### Wait... Why stop at issues?
301
302This is where the true power of GitRoot will blow your mind.
303
304Because GitRoot doesn't care about databases, and because everything is just a file inside Git, the `silo` plugin can aggregate anything. I don't need a separate `TODO` page for my tickets, because I already have one that automatically tracks the `//TODO` comments inside my actual source code.
305
306Yes, you read that right. You can visit it live at [https://gitroot.dev/boards/todos.html](../boards/todos.md).
307
308Every time I write a `//TODO` in a `.go`, `.rs` or `.ts` file, GitRoot parses it, aggregates it, and generates a dynamic dashboard. Tickets, documentation, source code comments... it's all the exact same data structure.
309
310To be clear, it doesn’t magically transform your source code comments into full-blown database-like issues with IDs and statuses. It simply extracts them to give you a centralized, living view of your technical debt, right alongside your official tickets. It’s the exact same data aggregation pipeline, tailored for two different use cases.
311
312---
313
314> If you want to discover more plugins and configurations, feel free to browse through the [🧩 Plugins section](../plugins/). However, for now, the best way to explore remains checking out the actual [GitRoot plugins.yml configuration file](../.gitroot/plugins.yml).