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).