diff --git a/docs/api-examples.md b/docs/api-examples.md
new file mode 100644
index 0000000..c369ee0
--- /dev/null
+++ b/docs/api-examples.md
@@ -0,0 +1,56 @@
+# API Examples
+
+Here are some common ways to interact with the `gitlab-lint` API using `curl`.
+
+## Rules
+
+Get all available rules:
+
+```bash
+curl http://localhost:3000/api/v1/rules
+```
+
+Get details about a specific rule and which projects violate it:
+
+```bash
+curl http://localhost:3000/api/v1/rules/without-readme
+```
+
+## Projects
+
+List all analyzed projects:
+
+```bash
+curl http://localhost:3000/api/v1/projects
+```
+
+Search for projects by name:
+
+```bash
+curl http://localhost:3000/api/v1/projects?q=my-project
+```
+
+Get a specific project with all its violations:
+
+```bash
+curl http://localhost:3000/api/v1/projects/123
+```
+
+## Levels
+
+See all severity levels:
+
+```bash
+curl http://localhost:3000/api/v1/levels
+```
+
+## Statistics
+
+Get overall statistics:
+
+```bash
+curl http://localhost:3000/api/v1/stats
+```
+
+This shows total projects analyzed, violations by severity level, and other metrics from the last collector run.
+
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..e328e28
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,43 @@
+# Architecture
+
+## How It Works
+
+`gitlab-lint` has two main parts: a collector that gathers data from GitLab and runs linting rules, and an API that serves that data.
+
+## Components
+
+### Collector
+
+The collector connects to your GitLab instance, fetches all projects, and runs each registered rule against them. When a rule fails for a project, that violation gets stored in MongoDB.
+
+It's designed to run periodically - you might set up a cron job to run it nightly, for example. This keeps your linting data fresh without manual intervention.
+
+### API
+
+The API is a straightforward REST service built with Echo. It reads from MongoDB and exposes endpoints for querying projects, rules, and statistics. The frontend (or any HTTP client) can consume these endpoints to display linting results.
+
+### Rules System
+
+Rules are the core of the linting logic. Each rule is a Go struct implementing the `Ruler` interface, which makes the system easy to extend. Want to check for something new? Just write a struct with a `Run` method and register it.
+
+The registry pattern means the collector doesn't need to know about individual rules - it just iterates through whatever's registered and runs them all.
+
+### Database
+
+We use MongoDB for storage. The database layer provides a clean abstraction over Mongo operations, so the collector and API don't deal with database specifics directly.
+
+## Data Flow
+
+When the collector runs, it:
+
+1. Authenticates with GitLab
+2. Fetches all projects (paginated)
+3. Spawns worker goroutines to process projects in parallel
+4. For each non-fork project, runs all registered rules
+5. Stores violations in MongoDB
+6. Saves statistics about the run
+
+The API then reads this data and serves it through REST endpoints. Simple and effective.
+
+See [diagrams](./diagrams.md) for visual representations of these flows.
+
diff --git a/docs/diagrams.md b/docs/diagrams.md
new file mode 100644
index 0000000..880965f
--- /dev/null
+++ b/docs/diagrams.md
@@ -0,0 +1,24 @@
+# Diagrams
+
+Visual representations of how `gitlab-lint` is structured and how data flows through the system.
+
+## System Architecture
+
+
+
+This shows the main pieces and how they connect. The collector pulls from GitLab, runs rules, and stores results in MongoDB. The API reads from Mongo and serves data to clients.
+
+## Collector Flow
+
+
+
+Here's what happens when you run the collector. It authenticates, fetches projects in pages, spawns workers to process them in parallel, and saves everything to the database. Forks are skipped since they're usually duplicates of upstream projects.
+
+## Rule Processing
+
+
+
+This sequence diagram shows how rules get executed. For each project, the collector runs every registered rule. If a rule's `Run` method returns true (meaning the project violates that rule), a violation record goes into the database.
+
+The nice thing about this design is you can add new rules without touching the collector code - just implement the interface and register it.
+
diff --git a/docs/diagrams/architecture.mmd b/docs/diagrams/architecture.mmd
new file mode 100644
index 0000000..c824058
--- /dev/null
+++ b/docs/diagrams/architecture.mmd
@@ -0,0 +1,32 @@
+graph TB
+ subgraph GitLab
+ GL[GitLab API]
+ end
+
+ subgraph Collector
+ C[Collector Worker]
+ R[Rules Registry]
+ end
+
+ subgraph Database
+ DB[(MongoDB)]
+ end
+
+ subgraph API
+ API_SERVER[API Server
Echo Framework]
+ ENDPOINTS[Endpoints
/projects
/rules
/stats]
+ end
+
+ subgraph Clients
+ FRONTEND[Frontend React]
+ CLI[CLI/curl]
+ end
+
+ GL -->|Fetch Projects| C
+ C -->|Execute Rules| R
+ C -->|Store Results| DB
+ DB -->|Read Data| API_SERVER
+ API_SERVER --> ENDPOINTS
+ ENDPOINTS -->|JSON| FRONTEND
+ ENDPOINTS -->|JSON| CLI
+
diff --git a/docs/diagrams/architecture.png b/docs/diagrams/architecture.png
new file mode 100644
index 0000000..4ee1112
Binary files /dev/null and b/docs/diagrams/architecture.png differ
diff --git a/docs/diagrams/collector-flow.mmd b/docs/diagrams/collector-flow.mmd
new file mode 100644
index 0000000..c0ca4e7
--- /dev/null
+++ b/docs/diagrams/collector-flow.mmd
@@ -0,0 +1,19 @@
+flowchart TD
+ START([Collector Started]) --> AUTH[Authenticate with GitLab API]
+ AUTH --> FETCH[Fetch Project List]
+ FETCH --> CHECK{More Projects?}
+ CHECK -->|Yes| WORKER[Create Worker Goroutine]
+ WORKER --> LOOP[For Each Project]
+ LOOP --> FORK{Is Fork?}
+ FORK -->|Yes| SKIP[Skip Project]
+ FORK -->|No| RULES[Execute All Rules]
+ RULES --> STORE[Store Violations]
+ STORE --> NEXT[Next Project]
+ NEXT --> LOOP
+ SKIP --> NEXT
+ CHECK -->|No| WAIT[Wait for Workers]
+ WAIT --> SAVE_RULES[Save Rules to DB]
+ SAVE_RULES --> SAVE_PROJECTS[Save Projects to DB]
+ SAVE_PROJECTS --> SAVE_STATS[Save Statistics]
+ SAVE_STATS --> END([Collector Finished])
+
diff --git a/docs/diagrams/collector-flow.png b/docs/diagrams/collector-flow.png
new file mode 100644
index 0000000..c5e7bce
Binary files /dev/null and b/docs/diagrams/collector-flow.png differ
diff --git a/docs/diagrams/rule-processing.mmd b/docs/diagrams/rule-processing.mmd
new file mode 100644
index 0000000..560f7cb
--- /dev/null
+++ b/docs/diagrams/rule-processing.mmd
@@ -0,0 +1,24 @@
+sequenceDiagram
+ participant C as Collector
+ participant R as Rules Registry
+ participant Rule as Rule (Ruler)
+ participant GL as GitLab Client
+ participant P as Project
+ participant DB as MongoDB
+
+ C->>R: GetAllRules()
+ R-->>C: []Ruler
+ loop For Each Project
+ C->>GL: GetProject(id)
+ GL-->>C: Project
+ loop For Each Rule
+ C->>Rule: Run(client, project)
+ Rule->>P: Check Condition
+ P-->>Rule: Result
+ Rule-->>C: bool (violates?)
+ alt Violation Detected
+ C->>DB: InsertViolation(rule, project)
+ end
+ end
+ end
+
diff --git a/docs/diagrams/rule-processing.png b/docs/diagrams/rule-processing.png
new file mode 100644
index 0000000..ba7c21d
Binary files /dev/null and b/docs/diagrams/rule-processing.png differ
diff --git a/docs/tutorial-creating-rules.md b/docs/tutorial-creating-rules.md
new file mode 100644
index 0000000..211b41e
--- /dev/null
+++ b/docs/tutorial-creating-rules.md
@@ -0,0 +1,135 @@
+# Creating a New Rule
+
+This guide walks through adding a new linting rule to `gitlab-lint`. We'll build a rule called `WithoutLicense` that checks whether projects have a license file.
+
+## Understanding Rules
+
+Rules in `gitlab-lint` implement the `Ruler` interface:
+
+```go
+type Ruler interface {
+ Run(client *gitlab.Client, p *gitlab.Project) bool
+ GetSlug() string
+ GetLevel() string
+}
+```
+
+The `Run` method contains your rule's logic and returns `true` when a project violates the rule. `GetSlug` provides a unique identifier, and `GetLevel` sets the severity.
+
+## Writing the Rule
+
+Create `rules/without_license.go`:
+
+```go
+// Copyright (c) 2021, Marcelo Jorge Vieira
+// Licensed under the BSD 3-Clause License
+
+package rules
+
+import "github.com/xanzy/go-gitlab"
+
+type WithoutLicense struct {
+ Description string `json:"description"`
+ ID string `json:"ruleId"`
+ Level string `json:"level"`
+ Name string `json:"name"`
+}
+
+func (w *WithoutLicense) Run(c *gitlab.Client, p *gitlab.Project) bool {
+ return p.LicenseURL == ""
+}
+
+func (w *WithoutLicense) GetSlug() string {
+ return "without-license"
+}
+
+func (w *WithoutLicense) GetLevel() string {
+ return LevelWarning
+}
+
+func NewWithoutLicense() Ruler {
+ w := &WithoutLicense{
+ Name: "Without License",
+ Description: "Projects without a LICENSE file are not recommended.",
+ }
+ w.ID = w.GetSlug()
+ w.Level = w.GetLevel()
+ return w
+}
+```
+
+The logic is straightforward - GitLab's API provides `LicenseURL` when a license exists, so we just check if it's empty.
+
+## Adding Tests
+
+Create `rules/without_license_test.go`:
+
+```go
+// Copyright (c) 2021, Marcelo Jorge Vieira
+// Licensed under the BSD 3-Clause License
+
+package rules
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/xanzy/go-gitlab"
+)
+
+func TestWithoutLicense(t *testing.T) {
+ tests := []struct {
+ name string
+ project *gitlab.Project
+ want bool
+ }{
+ {
+ name: "should return true if project does not have a license",
+ project: &gitlab.Project{
+ LicenseURL: "",
+ },
+ want: true,
+ },
+ {
+ name: "should return false if project has a license",
+ project: &gitlab.Project{
+ LicenseURL: "https://opensource.org/licenses/BSD-3-Clause",
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ w := &WithoutLicense{}
+ got := w.Run(nil, tt.project)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+```
+
+Run `make test` to verify everything works.
+
+## Registering the Rule
+
+Add your rule to `rules/my_registry.go`:
+
+```go
+func init() {
+ MyRegistry.AddRule(NewEmptyRepository())
+ MyRegistry.AddRule(NewGoVendorFolder())
+ // ... other rules
+ MyRegistry.AddRule(NewWithoutLicense())
+}
+```
+
+## Testing It Out
+
+Start the server with `make run-docker` and check the rules endpoint:
+
+```bash
+curl http://localhost:3000/api/v1/rules
+```
+
+You should see `without-license` in the response. That's it - your rule is now part of the system and will run on all projects during the next collection.
+
diff --git a/rules/my_registry.go b/rules/my_registry.go
index 49b52b0..a8f7f7a 100644
--- a/rules/my_registry.go
+++ b/rules/my_registry.go
@@ -15,3 +15,6 @@ func init() {
MyRegistry.AddRule(NewWithoutReadme())
MyRegistry.AddRule(NewWithoutMakefile())
}
+
+ MyRegistry.AddRule(NewWithoutLicense())
+
diff --git a/rules/without_license.go b/rules/without_license.go
new file mode 100644
index 0000000..cd04399
--- /dev/null
+++ b/rules/without_license.go
@@ -0,0 +1,36 @@
+// Copyright (c) 2021, Marcelo Jorge Vieira
+// Licensed under the BSD 3-Clause License
+
+package rules
+
+import "github.com/xanzy/go-gitlab"
+
+type WithoutLicense struct {
+ Description string `json:"description"`
+ ID string `json:"ruleId"`
+ Level string `json:"level"`
+ Name string `json:"name"`
+}
+
+func (w *WithoutLicense) Run(c *gitlab.Client, p *gitlab.Project) bool {
+ return p.LicenseURL == ""
+}
+
+func (w *WithoutLicense) GetSlug() string {
+ return "without-license"
+}
+
+func (w *WithoutLicense) GetLevel() string {
+ return LevelWarning
+}
+
+func NewWithoutLicense() Ruler {
+ w := &WithoutLicense{
+ Name: "Without License",
+ Description: "Projects without a LICENSE file are not recommended.",
+ }
+ w.ID = w.GetSlug()
+ w.Level = w.GetLevel()
+ return w
+}
+
diff --git a/rules/without_license_test.go b/rules/without_license_test.go
new file mode 100644
index 0000000..e959caa
--- /dev/null
+++ b/rules/without_license_test.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2021, Marcelo Jorge Vieira
+// Licensed under the BSD 3-Clause License
+
+package rules
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/xanzy/go-gitlab"
+)
+
+func TestWithoutLicense(t *testing.T) {
+ tests := []struct {
+ name string
+ project *gitlab.Project
+ want bool
+ }{
+ {
+ name: "should return true if project does not have a license",
+ project: &gitlab.Project{
+ LicenseURL: "",
+ },
+ want: true,
+ },
+ {
+ name: "should return false if project has a license",
+ project: &gitlab.Project{
+ LicenseURL: "https://opensource.org/licenses/BSD-3-Clause",
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ w := &WithoutLicense{}
+ got := w.Run(nil, tt.project)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+