diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@
 .eggs/
 __pycache__
 *.egg-info/
-version.txt
\ No newline at end of file
+version.txt
+.tox
+docs/apidoc
diff --git a/docs/Makefile b/docs/Makefile
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -3,17 +3,23 @@
 SOURCEDIR = .
 BUILDDIR = _build
 HTMLDIR = $(BUILDDIR)/html
+SWHPKGDIR = `python3 -c 'import swh; print(swh.__path__[0])'`
 
 INSTALL_HOST = pergamon.internal.softwareheritage.org
 INSTALL_DIR = /srv/softwareheritage/docs/webroot/devel
 INSTALL_GROUP = swhdev
 INSTALL_PERMS = g+rwX
 
-html: fix-indices-stamp sphinx/html
+SPHINXAPIDOC = sphinx-apidoc
+APIDOC_DIR = apidoc
+APIDOC_OPTS = --ext-viewcode
+APIDOC_EXCLUDES = */tests */tests/* */*/tests/* */*/*/tests/*
+APIDOC_EXCLUDES += */migrations */migrations/* */*/migrations/* */*/*/migrations/*
+APIDOC_SWH_EXCLUDES = $(patsubst %,$(SWHPKGDIR)/%,$(APIDOC_EXCLUDES))
 
-fix-indices-stamp: sphinx/html
-	bin/copy-and-fix-subprojects-indices
-	touch $@
+apidoc_dep = apidoc-stamp
+
+html: sphinx/html
 
 sphinx/html: links-stamp apidoc-stamp images-stamp rec-build-stamp
 
@@ -21,8 +27,9 @@
 	bin/ln-sphinx-subprojects
 	touch $@
 
+apidoc: $(apidoc_dep)
 apidoc-stamp:
-	$(MAKE) -C ../../ docs-apidoc
+	$(SPHINXAPIDOC) $(APIDOC_OPTS) -o $(APIDOC_DIR) $(SWHPKGDIR) $(APIDOC_SWH_EXCLUDES)
 	touch $@
 
 images-stamp:
@@ -33,13 +40,14 @@
 # non-sphinx managed documentation artifacts (e.g., schema diagrams) are also
 # built.
 rec-build-stamp: $(wildcard ../../swh-*/docs/*.rst)
-	$(MAKE) -C ../../ docs
+	$(MAKE) -C ../../ docs-assets
 	touch $@
 
 clean: sphinx/clean
 	bin/ln-sphinx-subprojects --remove
 	$(MAKE) -C images clean
 	rm -f *-stamp
+	rm -f $(APIDOC_DIR)/*
 
 distclean: clean
 	make -C ../../ docs-clean
diff --git a/requirements-swh-dev.txt b/requirements-swh-dev.txt
new file mode 100644
--- /dev/null
+++ b/requirements-swh-dev.txt
@@ -0,0 +1,22 @@
+# Add here internal Software Heritage dependencies, one per line.
+../swh-core
+../swh-model
+../swh-objstorage[testing]
+../swh-scheduler
+../swh-storage[schemadata]
+../swh-loader-core
+../swh-lister
+../swh-journal
+../swh-vault
+../swh-loader-dir
+../swh-loader-tar
+../swh-loader-pypi
+../swh-loader-debian
+../swh-loader-mercurial
+../swh-loader-svn
+../swh-loader-git
+../swh-archiver
+../swh-web
+../swh-deposit
+../swh-indexer
+../swh-mirror-forge
diff --git a/requirements-swh.txt b/requirements-swh.txt
--- a/requirements-swh.txt
+++ b/requirements-swh.txt
@@ -1 +1,22 @@
 # Add here internal Software Heritage dependencies, one per line.
+swh.archiver
+swh.lister
+swh.loader.mercurial
+swh.model
+swh.web
+swh.core
+swh.loader.core
+swh.loader.pypi
+swh.objstorage[testing]
+swh.deposit
+swh.loader.debian
+swh.loader.svn
+swh.scheduler
+swh.indexer
+swh.loader.dir
+swh.loader.tar
+swh.storage[schemadata]
+swh.journal
+swh.loader.git
+swh.mirror.forge
+swh.vault
diff --git a/swh/docs/django_settings.py b/swh/docs/django_settings.py
new file mode 100644
--- /dev/null
+++ b/swh/docs/django_settings.py
@@ -0,0 +1,14 @@
+from swh.deposit.settings.development import *  # noqa
+import swh.web.settings.development as web
+
+
+# merge some config variables
+ns = globals()
+for var in dir(web):
+    if var.isupper() and var in ns and isinstance(ns[var], list):
+        for elt in getattr(web, var):
+            if elt not in ns[var]:
+                ns[var].append(elt)
+
+
+SECRET_KEY = 'change me'
diff --git a/swh/docs/sphinx/conf.py b/swh/docs/sphinx/conf.py
--- a/swh/docs/sphinx/conf.py
+++ b/swh/docs/sphinx/conf.py
@@ -20,6 +20,7 @@
               'sphinxcontrib.httpdomain',
               'sphinx.ext.extlinks',
               'sphinxcontrib.images',
+              'sphinx.ext.viewcode',
               ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -40,8 +41,9 @@
 
 # A string of reStructuredText that will be included at the beginning of every
 # source file that is read.
+# A bit hackish but should work both for each swh package and the whole swh-doc
 rst_prolog = '''
-.. include:: /swh_substitutions
+.. include:: /../../swh-docs/docs/swh_substitutions
 '''
 
 # The version info for the project you're documenting, acts as replacement for
@@ -123,22 +125,15 @@
 autodoc_member_order = 'bysource'
 autodoc_mock_imports = ['rados']
 
+modindex_common_prefix = ['swh.']
+
 # for the extlinks extension, sub-projects should fill that dict
 extlinks = {}
 
 
 # hack to set the adequate django settings when building global swh doc
-# to avoid build errors
-def source_read_handler(app, docname, source):
-    if 'swh-deposit' in docname:
-        os.environ.setdefault('DJANGO_SETTINGS_MODULE',
-                              'swh.deposit.settings.development')
-        django.setup()
-    elif 'swh-web' in docname:
-        os.environ.setdefault('DJANGO_SETTINGS_MODULE',
-                              'swh.web.settings.development')
-        django.setup()
-
-
+# to avoid autodoc build errors
 def setup(app):
-    app.connect('source-read', source_read_handler)
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE',
+                          'swh.docs.django_settings')
+    django.setup()
diff --git a/tox.ini b/tox.ini
new file mode 100644
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,28 @@
+[tox]
+envlist=flake8
+
+[testenv]
+basepython = python3
+
+[testenv:sphinx]
+deps =
+  django < 2
+  .[testing]
+  pifpaf
+commands =
+  {envpython} -m pifpaf run postgresql -- make -C docs html
+
+[testenv:sphinx-dev]
+deps =
+  django < 2
+  -rrequirements-swh-dev.txt
+  pifpaf
+commands =
+  {envpython} -m pifpaf run postgresql -- make -C docs html
+
+[testenv:flake8]
+skip_install = true
+deps =
+  flake8
+commands =
+  {envpython} -m flake8