diff --git a/source/apps/ctf/admin.py b/source/apps/ctf/admin.py
index 8c38f3f..5a74873 100644
--- a/source/apps/ctf/admin.py
+++ b/source/apps/ctf/admin.py
@@ -1,3 +1,17 @@
 from django.contrib import admin
 
-# Register your models here.
+from source.apps.ctf import models
+
+
+@admin.register(models.Team)
+class TeamAdmin(admin.ModelAdmin):
+    list_display = ("id", "name")
+
+@admin.register(models.Flag)
+class FlagAdmin(admin.ModelAdmin):
+    list_display = ("id", "name", "type")
+
+
+@admin.register(models.Hint)
+class HintAdmin(admin.ModelAdmin):
+    list_display = ("id", "flag")
diff --git a/source/apps/ctf/apps.py b/source/apps/ctf/apps.py
index 72da749..a24299d 100644
--- a/source/apps/ctf/apps.py
+++ b/source/apps/ctf/apps.py
@@ -3,4 +3,4 @@ from django.apps import AppConfig
 
 class CtfConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
-    name = 'apps.ctf'
+    name = 'source.apps.ctf'
diff --git a/source/apps/ctf/forms.py b/source/apps/ctf/forms.py
new file mode 100644
index 0000000..b059663
--- /dev/null
+++ b/source/apps/ctf/forms.py
@@ -0,0 +1,9 @@
+from django import forms
+
+
+class TeamForm(forms.Form):
+    name = forms.CharField(label="Team's name", max_length=32)
+
+
+class FlagForm(forms.Form):
+    name = forms.UUIDField(label="Identifier", max_length=32)
diff --git a/source/apps/ctf/models.py b/source/apps/ctf/models.py
index 71a8362..9c0c276 100644
--- a/source/apps/ctf/models.py
+++ b/source/apps/ctf/models.py
@@ -1,3 +1,65 @@
+import uuid
+
 from django.db import models
 
-# Create your models here.
+
+class Team(models.Model):
+    """
+    Represent a team participating in the event
+    """
+
+    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+    name = models.CharField(max_length=32)
+
+    validated_flags = models.ManyToManyField(to="Flag", blank=True, related_name="validated_by_teams")
+
+    def __str__(self):
+        return f"<{self.__class__.__name__}({self.name!r}, {self.id!r})>"
+
+
+class Flag(models.Model):
+    """
+    Represent a flag that can be validated by the team
+    """
+
+    class FlagType(models.IntegerChoices):
+        NORMAL = 0
+        BONUS = 1
+        MALUS = 2
+
+    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+
+    # the flag might depend on other flags
+    parents = models.ManyToManyField(
+        "self",
+        blank=True,
+        symmetrical=False,
+        related_name="children",
+    )
+
+    # information about the flags
+    # TODO(Faraphel): store code as hash ?
+    code = models.CharField(max_length=32)  # the password that must be given to obtain the flag
+    name = models.CharField(max_length=32)  # the name of the flag
+    description = models.TextField()  # the description on how to obtain the flag
+
+    # TODO(Faraphel): replace with or add a points amount you gain ?
+    type = models.IntegerField(choices=FlagType.choices, default=FlagType.NORMAL)
+
+    def __str__(self):
+        return f"<{self.__class__.__name__}({self.name!r}, {self.id!r})>"
+
+
+class Hint(models.Model):
+    """
+    Represent a hint that can be given to a team
+    """
+
+    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+    importance = models.PositiveIntegerField()
+    description = models.TextField()
+    penalty = models.PositiveIntegerField()
+    flag = models.ForeignKey(to=Flag, on_delete=models.CASCADE, related_name="hints")
+
+    def __str__(self):
+        return f"<{self.__class__.__name__}({self.flag.name!r}, {self.id!r})>"
diff --git a/source/apps/ctf/templates/ctf/base/base.html b/source/apps/ctf/templates/ctf/base/base.html
index 35ddc74..8ebfeb3 100644
--- a/source/apps/ctf/templates/ctf/base/base.html
+++ b/source/apps/ctf/templates/ctf/base/base.html
@@ -2,6 +2,7 @@
 <html lang="en">
 <head>
     <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{% block title %}{% endblock %}</title>
     {% block head %}
     {% endblock %}
diff --git a/source/apps/ctf/templates/ctf/flag_list.html b/source/apps/ctf/templates/ctf/flag_list.html
new file mode 100644
index 0000000..8704fe7
--- /dev/null
+++ b/source/apps/ctf/templates/ctf/flag_list.html
@@ -0,0 +1,105 @@
+{% extends "ctf/base/base.html" %}
+
+{% block title %}Flags{% endblock %}
+
+{% block head %}
+    <script src="https://unpkg.com/@popperjs/core@2"></script>
+    <script src="https://unpkg.com/tippy.js@6"></script>
+    <script src="https://cytoscape.org/cytoscape.js-popper/cytoscape-popper.js"></script>
+    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
+
+    <style>
+        #cy {
+            width: 100vw;
+            height: 100vh;
+        }
+    </style>
+{% endblock %}
+
+{% block body %}
+    <div id="cy"></div>
+    <script>
+        function tippyFactory(reference, content) {
+            // create a basic element for the tips object
+            const element = document.createElement('div');
+
+            // build the tip inside
+            return tippy(element, {
+                getReferenceClientRect: reference.getBoundingClientRect,
+                trigger: "manual",
+                content: content,
+
+                arrow: true,
+                placement: "bottom",
+                hideOnClick: false,
+                sticky: "reference",
+
+                interactive: true,
+                appendTo: cy.container()
+            });
+        }
+        // use the factory when creating tips
+        cytoscape.use(cytoscapePopper(tippyFactory));
+
+        // create a graph based on our data
+        const cy = cytoscape({
+            container: document.getElementById('cy'),
+            // content
+            elements: [
+                // nodes
+                {% for flag in flags %}
+                    {data: {
+                        id: "{{ flag.id }}",
+                        name: "{{ flag.name }}",
+                        description: "{{ flag.description }}",
+                        teams: [
+                            {% for team in flag.validated_by_teams.all %}
+                            "{{ team.name }}",
+                            {% endfor %}
+                        ],
+                    }},
+                {% endfor %}
+
+                // links
+                {% for flag in flags %}
+                    {% for child_flag in flag.children.all %}
+                        {data: {source: "{{ child_flag.id }}", target: "{{ flag.id }}"}},
+                    {% endfor %}
+                {% endfor %}
+            ],
+            // style
+            style: [
+                {selector: 'node', style: {'label': 'data(name)'}},
+                {selector: 'edge', style: {'target-arrow-shape': 'triangle'}},
+            ],
+            // layout
+            layout: {
+              name: 'breadthfirst',
+              directed: true
+            }
+        });
+
+        // show the description of the nodes on hover
+        cy.ready(() => {
+            cy.nodes().forEach(node => {
+                // build the tip
+                const tippy = node.popper({
+                    content: () => {
+                        const div = document.createElement('div');
+                        div.innerHTML = node.data("description") + "<br/>---<br/>" + node.data("teams");
+                        return div;
+                    },
+                    trigger: 'manual',
+                    interactive: true,
+                    appendTo: cy.container()
+                });
+
+                // events
+                node.on('mouseover', () => tippy.show());
+                node.on('mouseout', () => tippy.hide());
+            });
+        });
+    </script>
+{% endblock %}
+
+
diff --git a/source/apps/ctf/templates/ctf/flag_submit.html b/source/apps/ctf/templates/ctf/flag_submit.html
new file mode 100644
index 0000000..ff4e8f0
--- /dev/null
+++ b/source/apps/ctf/templates/ctf/flag_submit.html
@@ -0,0 +1,11 @@
+{% extends "ctf/base/base.html" %}
+
+{% block title %}Flag - Submit{% endblock %}
+
+{% block body %}
+    <form method="POST">
+        {% csrf_token %}
+        {{ form }}
+        <input type="submit">
+    </form>
+{% endblock %}
\ No newline at end of file
diff --git a/source/apps/ctf/templates/ctf/team_create.html b/source/apps/ctf/templates/ctf/team_create.html
new file mode 100644
index 0000000..67d9789
--- /dev/null
+++ b/source/apps/ctf/templates/ctf/team_create.html
@@ -0,0 +1,11 @@
+{% extends "ctf/base/base.html" %}
+
+{% block title %}Team - Create{% endblock %}
+
+{% block body %}
+    <form method="POST">
+        {% csrf_token %}
+        {{ form }}
+        <input type="submit">
+    </form>
+{% endblock %}
diff --git a/source/apps/ctf/templates/ctf/team_list.html b/source/apps/ctf/templates/ctf/team_list.html
new file mode 100644
index 0000000..c03f003
--- /dev/null
+++ b/source/apps/ctf/templates/ctf/team_list.html
@@ -0,0 +1,13 @@
+{% extends "ctf/base/base.html" %}
+
+{% block title %}Teams{% endblock %}
+
+{% block body %}
+    <ul>
+    {% for team in teams %}
+        <li>{{ team.name }}</li>
+    {% endfor %}
+    </ul>
+
+    <a href="{% url "team_create" %}">Create</a>
+{% endblock %}
diff --git a/source/apps/ctf/urls.py b/source/apps/ctf/urls.py
index 3a7d2a0..e7e276a 100644
--- a/source/apps/ctf/urls.py
+++ b/source/apps/ctf/urls.py
@@ -3,5 +3,9 @@ from django.urls import path
 from source.apps.ctf import views
 
 urlpatterns = [
-    path("", views.homepage, name='homepage'),
+    path("", views.view_homepage, name='homepage'),
+    path("teams", views.view_teams, name='teams'),
+    path("teams/create", views.view_team_create, name='team_create'),
+    path("flags", views.view_flags, name='flags'),
+    path("flags/submit", views.view_flag_submit, name='flag_submit'),
 ]
diff --git a/source/apps/ctf/views.py b/source/apps/ctf/views.py
index 10888f3..b4971f5 100644
--- a/source/apps/ctf/views.py
+++ b/source/apps/ctf/views.py
@@ -1,5 +1,43 @@
-from django.shortcuts import render
+from django.shortcuts import render, redirect
+
+from source.apps.ctf import forms, models
+
 
 # Create your views here.
-async def homepage(request):
+async def view_homepage(request):
     return render(request, "ctf/homepage.html")
+
+
+async def view_teams(request):
+    teams = [team async for team in models.Team.objects.all()]
+    return render(request, "ctf/team_list.html", context={"teams": teams})
+
+
+async def view_team_create(request):
+    if request.method == "POST":
+        form = forms.TeamForm(request.POST)
+        if form.is_valid():
+            # TODO(Faraphel): additional password to prevent unwanted team creation ?
+            await models.Team.objects.acreate(name=form.cleaned_data["name"])
+            return redirect("/")
+
+    else:
+        form = forms.TeamForm()
+
+    return render(request, "ctf/team_create.html", context={"form": form})
+
+
+def view_flags(request):
+    flags = models.Flag.objects.all()
+    return render(request, "ctf/flag_list.html", context={"flags": flags})
+
+
+async def view_flag_submit(request):
+    if request.method == "POST":
+        form = forms.FlagForm(request.POST)
+        if form.is_valid():
+            return redirect("/")
+    else:
+        form = forms.FlagForm()
+
+    return render(request, "ctf/flag_submit.html", context={"form": form})