diff --git a/develop/architecture.rst b/develop/architecture.rst
new file mode 100644
index 0000000..aeb3a24
--- /dev/null
+++ b/develop/architecture.rst
@@ -0,0 +1,127 @@
+*********************
+Software architecture
+*********************
+
+Limnoria abstracts its internal architecture away from plugins through
+its plugin API, which is enough most of the time.
+However, you may need want to understand its internal architecture, either
+to debug complex problems, provide advanced features that hook into
+the internals, contribute to the core, or simply out of curiosity.
+
+This guide will try to walk you through this, assuming you are already
+familiar with using the bot and writing plugins (if not, see the
+:ref:`capabilities` documentation, the :ref:`plugin-tutorial`,
+and :ref:`events`,
+
+You should also be somewhat familiar with the
+`IRC protocol `.
+
+.. note::
+
+ This document is a work in progress and is still incomplete.
+ As usually, feel free to ask any questions in #limnoria @ freenode.
+
+Main loop and drivers
+=====================
+
+The main event loop is a very classic synchronous loop.
+It is defined in :file:`scripts/supybot`, and essentially just this::
+
+ while world.ircs:
+ try:
+ drivers.run()
+ except KeyboardInterrupt:
+ # Handle Ctrl-C (trigger shutdown)
+ except:
+ # Handle other unhandled errors
+
+Where :func:`drivers.run` does this::
+
+ for (name, driver) in _drivers.items():
+ if name not in _deadDrivers:
+ driver.run()
+ for name in _deadDrivers:
+ # Remove the driver
+ while _newDrivers:
+ (name, driver) = _newDrivers.pop()
+ # add the new driver
+
+Drivers are the sources of events in the main thread. In a normal Limnoria
+setup, there are two types of drivers: the Socket driver (which connects
+synchronously to IRC) and the :ref:`schedule driver `
+(which runs functions periodically, like cron).
+Historically, there was an alternative driver to connect to IRC,
+based on Twisted. It was deprecated, then removed in 2019, because
+Python's ``socket`` module became as powerful as Twisted as it gained
+support for ``select()`` and TLS.
+
+Network drivers have a reference to a :class:`irclib.Irc` object, and
+do three things in their ``run()`` method:
+
+1. check the connection is still alive (and schedule a reconnect if not)
+2. get new messages from their :class:`irclib.Irc` instance (using
+ :meth:`irclib.Irc.takeMsg`) and send them to the network
+3. get new messages from the network and pass them to their :class:`irclib.Irc`
+ (using :meth:`irclib.Irc.feedMsg`)
+
+The actual implementation of the current ``Socket`` driver is actually
+a little more complex than this, as all ``Socket`` driver instances cooperate
+to use ``select()`` together, but this is the rough idea.
+See :file:`src/drivers/Socket.py` for details.
+
+irclib
+======
+
+As we saw above, network drivers pass their messages to a class defined in
+:mod:`irclib`, which is where most of the IRC protocol implementation is.
+
+Unlike most event-driven software (especially IRC implementation), Limnoria
+does not have hooks that are registered to call a function when a specific
+event/IRC command is received.
+Instead, event listeners receive all events, and inherit on
+:class:`supybot.irclib.IrcCommandDispatcher`, which calls a specific method
+based on the IRC command name. For example, it calls the ``doTopic`` method
+when receiving a ``TOPIC`` message.
+
+This dispatching is used both in the main IRC handling
+(:class:`supybot.irclib.Irc`) and plugins (via
+:class:`supybot.callbacks.PluginMixin`, which inherits
+:class:`supybot.irclib.IrcCommandDispatcher`).
+
+We saw above that the :class:`supybot.irclib.Irc` object receives messages
+directly from the driver. It's also in charge of keeping track of other
+callbacks (ie. plugins) via :meth:`supybot.irclib.Irc.addCallback` and passing
+every message to their ``__call__`` method (which then does the dispatching
+on its own again, as it inherits :class:`supybot.irclib.IrcCommandDispatcher`).
+
+As there are few callbacks (under a hundred plugins),
+this simple architecture is efficient enough.
+
+Additionally, when receiving a message and before sending one, it iterates
+through the list of plugins and calls their ``inFilter`` and ``outFilter``
+methods (respectively), if any.
+
+If you look at the code of :class:`supybot.irclib.Irc` and
+:class:`supybot.irclib.IrcState`, you see they are mostly made of ``doXxx``
+methods, which exhaustively implement every known IRC command, update some
+state, and optionally react to it by queuing messages.
+
+Commands
+========
+
+Next is the callbacks system, mostly implemented in :mod:``supybot.callbacks``.
+This is where all the magic happens to make plugins so easy to write;
+it's also the most complex part of Limnoria and the hardest to understand,
+because everything is tightly interleaved.
+
+TODO
+
+Registry
+========
+
+TODO
+
+Auto-documentation
+==================
+
+TODO
diff --git a/develop/events.rst b/develop/events.rst
index 557df71..a86c0a4 100644
--- a/develop/events.rst
+++ b/develop/events.rst
@@ -1,3 +1,5 @@
+.. _events:
+
***********************************
Special methods and catching events
***********************************
diff --git a/develop/index.rst b/develop/index.rst
index 16926f0..0206572 100644
--- a/develop/index.rst
+++ b/develop/index.rst
@@ -19,6 +19,7 @@ Plugin Developer Guide
events.rst
httpserver.rst
schedule.rst
+ architecture.rst
faq.rst
diff --git a/develop/plugin_tutorial.rst b/develop/plugin_tutorial.rst
index 04fecc8..94cec1c 100644
--- a/develop/plugin_tutorial.rst
+++ b/develop/plugin_tutorial.rst
@@ -1,3 +1,5 @@
+.. _plugin-tutorial:
+
**********************************
Writing Your First Limnoria Plugin
**********************************