diff --git a/.gitignore b/.gitignore
index 1e1017c8d1309f6d805dd8dfef53def18929b1c8..8b511703444291a612645e398d5b568f6e717a6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,4 @@ front/selenium-debug.log
 docs/_build
 
 data/
+.env
diff --git a/CHANGELOG b/CHANGELOG
index 0b91987235f620475b00f26af87787c43670913b..15eacce4f09d9b5083bcd2625daf333102f3bb67 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,53 @@ Changelog
 
 .. towncrier
 
+Release notes:
+
+Preparing for federation
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+In order to prepare for federation (see #136 and #137), new API endpoints
+have been added under /federation and /.well-known/webfinger.
+
+For these endpoints to work, you will need to update your nginx configuration,
+and add the following snippets::
+
+    location /federation/ {
+        include /etc/nginx/funkwhale_proxy.conf;
+        proxy_pass   http://funkwhale-api/federation/;
+    }
+
+    location /.well-known/webfinger {
+        include /etc/nginx/funkwhale_proxy.conf;
+        proxy_pass   http://funkwhale-api/.well-known/webfinger;
+    }
+
+This will ensure federation endpoints will be reachable in the future.
+You can of course skip this part if you know you will not federate your instance.
+
+A new ``FEDERATION_ENABLED`` env var have also been added to control wether
+federation is enabled or not on the application side. This settings defaults
+to True, which should have no consequencies at the moment, since actual
+federation is not implemented and the only available endpoints are for
+testing purposes.
+
+Add ``FEDERATION_ENABLED=false`` to your .env file to disable federation
+on the application side.
+
+The last step involves generating RSA private and public keys for signing
+your instance requests on the federation. This can be done via::
+
+    # on docker setups
+    docker-compose --rm api python manage.py generate_keys --no-input
+
+    # on non-docker setups
+    source /srv/funkwhale/virtualenv/bin/activate
+    source /srv/funkwhale/load_env
+    python manage.py generate_keys --no-input
+
+That's it :)
+
+
 0.7 (2018-03-21)
 ----------------
 
diff --git a/README.rst b/README.rst
index 93281d26fb6abd65d0590183a416236a585b68e9..2d5d2011d2f368332458bd567284e2121a4dfca8 100644
--- a/README.rst
+++ b/README.rst
@@ -73,6 +73,19 @@ via the following command::
     docker-compose -f dev.yml build
 
 
+Creating your env file
+^^^^^^^^^^^^^^^^^^^^^^
+
+We provide a working .env.dev configuration file that is suitable for
+development. However, to enable customization on your machine, you should
+also create a .env file that will hold your personal environment
+variables (those will not be commited to the project).
+
+Create it like this::
+
+    touch .env
+
+
 Database management
 ^^^^^^^^^^^^^^^^^^^
 
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index a6a46a85a2f1394197a53597ea1fc215d1b20165..32cdb5b7f53e54dedcab0a3665d647e349553170 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/dev/ref/settings/
 """
 from __future__ import absolute_import, unicode_literals
 
+from urllib.parse import urlsplit
 import os
 import environ
 from funkwhale_api import __version__
@@ -24,8 +25,13 @@ try:
 except FileNotFoundError:
     pass
 
-ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
 FUNKWHALE_URL = env('FUNKWHALE_URL')
+FUNKWHALE_HOSTNAME = urlsplit(FUNKWHALE_URL).netloc
+
+FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True)
+FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME)
+
+ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
 
 # APP CONFIGURATION
 # ------------------------------------------------------------------------------
@@ -395,4 +401,5 @@ ACCOUNT_USERNAME_BLACKLIST = [
     'owner',
     'superuser',
     'staff',
+    'service',
 ] + env.list('ACCOUNT_USERNAME_BLACKLIST', default=[])
diff --git a/api/config/urls.py b/api/config/urls.py
index 8f7e37bc26ae56ba9967682f4ec3f19f04cc71f4..90598ea841f474e5b887fda7fe42f23975cd4c00 100644
--- a/api/config/urls.py
+++ b/api/config/urls.py
@@ -13,6 +13,9 @@ urlpatterns = [
     url(settings.ADMIN_URL, admin.site.urls),
 
     url(r'^api/', include(("config.api_urls", 'api'), namespace="api")),
+    url(r'^', include(
+        ('funkwhale_api.federation.urls', 'federation'),
+        namespace="federation")),
     url(r'^api/v1/auth/', include('rest_auth.urls')),
     url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
     url(r'^accounts/', include('allauth.urls')),
diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..83d0285be263d228ffbb564a9bfc9f898fe77dbf
--- /dev/null
+++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py
@@ -0,0 +1,34 @@
+from django.forms import widgets
+
+from dynamic_preferences import types
+from dynamic_preferences.registries import global_preferences_registry
+
+federation = types.Section('federation')
+
+
+@global_preferences_registry.register
+class FederationPrivateKey(types.StringPreference):
+    show_in_api = False
+    section = federation
+    name = 'private_key'
+    default = ''
+    help_text = (
+        'Instance private key, used for signing federation HTTP requests'
+    )
+    verbose_name = (
+        'Instance private key (keep it secret, do not change it)'
+    )
+
+
+@global_preferences_registry.register
+class FederationPublicKey(types.StringPreference):
+    show_in_api = False
+    section = federation
+    name = 'public_key'
+    default = ''
+    help_text = (
+        'Instance public key, used for signing federation HTTP requests'
+    )
+    verbose_name = (
+        'Instance public key (do not change it)'
+    )
diff --git a/api/funkwhale_api/federation/exceptions.py b/api/funkwhale_api/federation/exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..96fd24a7ed7eb2962b52aaccb578957911647249
--- /dev/null
+++ b/api/funkwhale_api/federation/exceptions.py
@@ -0,0 +1,4 @@
+
+
+class MalformedPayload(ValueError):
+    pass
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index 29aed4baf728a316cb0398537a7cbbed0bb961ab..f5d612b0dad5133197fb4385b583833539920126 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -4,16 +4,16 @@ import requests_http_signature
 
 from funkwhale_api.factories import registry
 
-from . import signing
+from . import keys
 
 
-registry.register(signing.get_key_pair, name='federation.KeyPair')
+registry.register(keys.get_key_pair, name='federation.KeyPair')
 
 
 @registry.register(name='federation.SignatureAuth')
 class SignatureAuthFactory(factory.Factory):
     algorithm = 'rsa-sha256'
-    key = factory.LazyFunction(lambda: signing.get_key_pair()[0])
+    key = factory.LazyFunction(lambda: keys.get_key_pair()[0])
     key_id = factory.Faker('url')
 
     class Meta:
diff --git a/api/funkwhale_api/federation/keys.py b/api/funkwhale_api/federation/keys.py
new file mode 100644
index 0000000000000000000000000000000000000000..432560ef7446d5973d23850cff7a2cb253c9fba7
--- /dev/null
+++ b/api/funkwhale_api/federation/keys.py
@@ -0,0 +1,43 @@
+from cryptography.hazmat.primitives import serialization as crypto_serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.backends import default_backend as crypto_default_backend
+
+import requests
+
+from . import exceptions
+
+
+def get_key_pair(size=2048):
+    key = rsa.generate_private_key(
+        backend=crypto_default_backend(),
+        public_exponent=65537,
+        key_size=size
+    )
+    private_key = key.private_bytes(
+        crypto_serialization.Encoding.PEM,
+        crypto_serialization.PrivateFormat.PKCS8,
+        crypto_serialization.NoEncryption())
+    public_key = key.public_key().public_bytes(
+        crypto_serialization.Encoding.PEM,
+        crypto_serialization.PublicFormat.PKCS1
+    )
+
+    return private_key, public_key
+
+
+def get_public_key(actor_url):
+    """
+    Given an actor_url, request it and extract publicKey data from
+    the response payload.
+    """
+    response = requests.get(actor_url)
+    response.raise_for_status()
+    payload = response.json()
+    try:
+        return {
+            'public_key_pem': payload['publicKey']['publicKeyPem'],
+            'id': payload['publicKey']['id'],
+            'owner': payload['publicKey']['owner'],
+        }
+    except KeyError:
+        raise exceptions.MalformedPayload(str(payload))
diff --git a/api/funkwhale_api/federation/management/__init__.py b/api/funkwhale_api/federation/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/federation/management/commands/generate_keys.py b/api/funkwhale_api/federation/management/commands/generate_keys.py
new file mode 100644
index 0000000000000000000000000000000000000000..eafe9aae3477753a7b61cbc854152e3d95e26e59
--- /dev/null
+++ b/api/funkwhale_api/federation/management/commands/generate_keys.py
@@ -0,0 +1,53 @@
+from django.core.management.base import BaseCommand, CommandError
+from django.db import transaction
+
+from dynamic_preferences.registries import global_preferences_registry
+
+from funkwhale_api.federation import keys
+
+
+class Command(BaseCommand):
+    help = (
+        'Generate a public/private key pair for your instance,'
+        ' for federation purposes. If a key pair already exists, does nothing.'
+    )
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '--replace',
+            action='store_true',
+            dest='replace',
+            default=False,
+            help='Replace existing key pair, if any',
+        )
+        parser.add_argument(
+            '--noinput', '--no-input', action='store_false', dest='interactive',
+            help="Do NOT prompt the user for input of any kind.",
+        )
+
+    @transaction.atomic
+    def handle(self, *args, **options):
+        preferences = global_preferences_registry.manager()
+        existing_public = preferences['federation__public_key']
+        existing_private = preferences['federation__public_key']
+
+        if existing_public or existing_private and not options['replace']:
+            raise CommandError(
+                'Keys are already present! '
+                'Replace them with --replace if you know what you are doing.')
+
+        if options['interactive']:
+            message = (
+                'Are you sure you want to do this?\n\n'
+                "Type 'yes' to continue, or 'no' to cancel: "
+            )
+            if input(''.join(message)) != 'yes':
+                raise CommandError("Operation cancelled.")
+        private, public = keys.get_key_pair()
+        preferences['federation__public_key'] = public.decode('utf-8')
+        preferences['federation__private_key'] = private.decode('utf-8')
+
+        self.stdout.write(
+            'Your new key pair was generated.'
+            'Your public key is now:\n\n{}'.format(public.decode('utf-8'))
+        )
diff --git a/api/funkwhale_api/federation/renderers.py b/api/funkwhale_api/federation/renderers.py
new file mode 100644
index 0000000000000000000000000000000000000000..642b634628f2787044eae9b6a96b9e7004b05244
--- /dev/null
+++ b/api/funkwhale_api/federation/renderers.py
@@ -0,0 +1,9 @@
+from rest_framework.renderers import JSONRenderer
+
+
+class ActivityPubRenderer(JSONRenderer):
+    media_type = 'application/activity+json'
+
+
+class WebfingerRenderer(JSONRenderer):
+    media_type = 'application/jrd+json'
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
new file mode 100644
index 0000000000000000000000000000000000000000..62a4500db5f39e92bc9d9dc316deedcca2ab5d76
--- /dev/null
+++ b/api/funkwhale_api/federation/serializers.py
@@ -0,0 +1,21 @@
+from django.urls import reverse
+from django.conf import settings
+
+from . import utils
+
+
+def repr_instance_actor():
+    """
+    We do not use a serializer here, since it's pretty static
+    """
+    return {
+        '@context': [
+            'https://www.w3.org/ns/activitystreams',
+            'https://w3id.org/security/v1',
+            {},
+        ],
+        'id': utils.full_url(reverse('federation:instance-actor')),
+        'type': 'Service',
+        'inbox': utils.full_url(reverse('federation:instance-inbox')),
+        'outbox': utils.full_url(reverse('federation:instance-outbox')),
+    }
diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py
index e8d79097c5bf154e7f6edd4661b0f74d831241b8..87ac82bac0b9a5b599ce7d3721e8f4f876c02e8c 100644
--- a/api/funkwhale_api/federation/signing.py
+++ b/api/funkwhale_api/federation/signing.py
@@ -1,28 +1,6 @@
 import requests
 import requests_http_signature
 
-from cryptography.hazmat.primitives import serialization as crypto_serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.backends import default_backend as crypto_default_backend
-
-
-def get_key_pair(size=2048):
-    key = rsa.generate_private_key(
-        backend=crypto_default_backend(),
-        public_exponent=65537,
-        key_size=size
-    )
-    private_key = key.private_bytes(
-        crypto_serialization.Encoding.PEM,
-        crypto_serialization.PrivateFormat.PKCS8,
-        crypto_serialization.NoEncryption())
-    public_key = key.public_key().public_bytes(
-        crypto_serialization.Encoding.PEM,
-        crypto_serialization.PublicFormat.PKCS1
-    )
-
-    return private_key, public_key
-
 
 def verify(request, public_key):
     return requests_http_signature.HTTPSignatureAuth.verify(
diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c59a1414aee428deadbf0be58ec348bbf3f5317
--- /dev/null
+++ b/api/funkwhale_api/federation/urls.py
@@ -0,0 +1,15 @@
+from rest_framework import routers
+
+from . import views
+
+router = routers.SimpleRouter(trailing_slash=False)
+router.register(
+    r'instance',
+    views.InstanceViewSet,
+    'instance')
+router.register(
+    r'.well-known',
+    views.WellKnownViewSet,
+    'well-known')
+
+urlpatterns = router.urls
diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e83f54b5d54c79d684d80c057e7e0fbb7cd5d1f1
--- /dev/null
+++ b/api/funkwhale_api/federation/utils.py
@@ -0,0 +1,14 @@
+from django.conf import settings
+
+
+def full_url(path):
+    """
+    Given a relative path, return a full url usable for federation purpose
+    """
+    root = settings.FUNKWHALE_URL
+    if path.startswith('/') and root.endswith('/'):
+        return root + path[1:]
+    elif not path.startswith('/') and not root.endswith('/'):
+        return root + '/' + path
+    else:
+        return root + path
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f1ee36f76fada4bc3aa90e6941958fe1781f7a1
--- /dev/null
+++ b/api/funkwhale_api/federation/views.py
@@ -0,0 +1,74 @@
+from django import forms
+from django.conf import settings
+from django.http import HttpResponse
+
+from rest_framework import viewsets
+from rest_framework import views
+from rest_framework import response
+from rest_framework.decorators import list_route
+
+from . import renderers
+from . import serializers
+from . import webfinger
+
+
+class FederationMixin(object):
+    def dispatch(self, request, *args, **kwargs):
+        if not settings.FEDERATION_ENABLED:
+            return HttpResponse(status=405)
+        return super().dispatch(request, *args, **kwargs)
+
+
+class InstanceViewSet(FederationMixin, viewsets.GenericViewSet):
+    authentication_classes = []
+    permission_classes = []
+    renderer_classes = [renderers.ActivityPubRenderer]
+
+    @list_route(methods=['get'])
+    def actor(self, request, *args, **kwargs):
+        return response.Response(serializers.repr_instance_actor())
+
+    @list_route(methods=['get'])
+    def inbox(self, request, *args, **kwargs):
+        raise NotImplementedError()
+
+    @list_route(methods=['get'])
+    def outbox(self, request, *args, **kwargs):
+        raise NotImplementedError()
+
+
+class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet):
+    authentication_classes = []
+    permission_classes = []
+    renderer_classes = [renderers.WebfingerRenderer]
+
+    @list_route(methods=['get'])
+    def webfinger(self, request, *args, **kwargs):
+        try:
+            resource_type, resource = webfinger.clean_resource(
+                request.GET['resource'])
+            cleaner = getattr(webfinger, 'clean_{}'.format(resource_type))
+            result = cleaner(resource)
+        except forms.ValidationError as e:
+            return response.Response({
+                'errors': {
+                    'resource': e.message
+                }
+            }, status=400)
+        except KeyError:
+            return response.Response({
+                'errors': {
+                    'resource': 'This field is required',
+                }
+            }, status=400)
+
+        handler = getattr(self, 'handler_{}'.format(resource_type))
+        data = handler(result)
+
+        return response.Response(data)
+
+    def handler_acct(self, clean_result):
+        username, hostname = clean_result
+        if username == 'service':
+            return webfinger.serialize_system_acct()
+        return {}
diff --git a/api/funkwhale_api/federation/webfinger.py b/api/funkwhale_api/federation/webfinger.py
new file mode 100644
index 0000000000000000000000000000000000000000..a9281c2b596899b7094ca659994441f9212f24e8
--- /dev/null
+++ b/api/funkwhale_api/federation/webfinger.py
@@ -0,0 +1,52 @@
+from django import forms
+from django.conf import settings
+from django.urls import reverse
+
+from . import utils
+VALID_RESOURCE_TYPES = ['acct']
+
+
+def clean_resource(resource_string):
+    if not resource_string:
+        raise forms.ValidationError('Invalid resource string')
+
+    try:
+        resource_type, resource = resource_string.split(':', 1)
+    except ValueError:
+        raise forms.ValidationError('Missing webfinger resource type')
+
+    if resource_type not in VALID_RESOURCE_TYPES:
+        raise forms.ValidationError('Invalid webfinger resource type')
+
+    return resource_type, resource
+
+
+def clean_acct(acct_string):
+    try:
+        username, hostname = acct_string.split('@')
+    except ValueError:
+        raise forms.ValidationError('Invalid format')
+
+    if hostname != settings.FEDERATION_HOSTNAME:
+        raise forms.ValidationError('Invalid hostname')
+
+    if username != 'service':
+        raise forms.ValidationError('Invalid username')
+
+    return username, hostname
+
+
+def serialize_system_acct():
+    return {
+        'subject': 'acct:service@{}'.format(settings.FEDERATION_HOSTNAME),
+        'aliases': [
+            utils.full_url(reverse('federation:instance-actor'))
+        ],
+        'links': [
+            {
+                'rel': 'self',
+                'type': 'application/activity+json',
+                'href': utils.full_url(reverse('federation:instance-actor')),
+            }
+        ]
+    }
diff --git a/api/requirements/test.txt b/api/requirements/test.txt
index b909a51ab6c6d4697313e9a7e64741bc20933d93..e11f26ca75ad36ab5efe1f91d86cd51b04928f9b 100644
--- a/api/requirements/test.txt
+++ b/api/requirements/test.txt
@@ -10,3 +10,4 @@ pytest-mock
 pytest-sugar
 pytest-xdist
 pytest-cov
+requests-mock
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index e6bfd0f4ecc963119141aa5069e9e940ac74c30e..41c03856d83aff2da6fc9b82e6a38d0a00e33213 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -2,6 +2,7 @@ import factory
 import tempfile
 import shutil
 import pytest
+import requests_mock
 
 from django.contrib.auth.models import AnonymousUser
 from django.core.cache import cache as django_cache
@@ -148,3 +149,9 @@ def media_root(settings):
     settings.MEDIA_ROOT = tmp_dir
     yield settings.MEDIA_ROOT
     shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture
+def r_mock():
+    with requests_mock.mock() as m:
+        yield m
diff --git a/api/tests/federation/__init__.py b/api/tests/federation/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c533306821a24a664c260089c0f15201c5a9870
--- /dev/null
+++ b/api/tests/federation/test_commands.py
@@ -0,0 +1,14 @@
+from django.core.management import call_command
+
+
+def test_generate_instance_key_pair(preferences, mocker):
+    mocker.patch(
+        'funkwhale_api.federation.keys.get_key_pair',
+        return_value=(b'private', b'public'))
+    assert preferences['federation__public_key'] == ''
+    assert preferences['federation__private_key'] == ''
+
+    call_command('generate_keys', interactive=False)
+
+    assert preferences['federation__private_key'] == 'private'
+    assert preferences['federation__public_key'] == 'public'
diff --git a/api/tests/federation/test_keys.py b/api/tests/federation/test_keys.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c30c30b17ed6450e7ec133498f3788be16a9ee6
--- /dev/null
+++ b/api/tests/federation/test_keys.py
@@ -0,0 +1,16 @@
+from funkwhale_api.federation import keys
+
+
+def test_public_key_fetching(r_mock):
+    payload = {
+        'id': 'https://actor.mock/users/actor#main-key',
+        'owner': 'test',
+        'publicKeyPem': 'test_pem',
+    }
+    actor = 'https://actor.mock/'
+    r_mock.get(actor, json={'publicKey': payload})
+    r = keys.get_public_key(actor)
+
+    assert r['id'] == payload['id']
+    assert r['owner'] == payload['owner']
+    assert r['public_key_pem'] == payload['publicKeyPem']
diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py
index 5187faa52d02ab4af621f98b3b49295a5b35cd9a..dc678f749bb0d7063ff58842c24de2391261b9ef 100644
--- a/api/tests/federation/test_signing.py
+++ b/api/tests/federation/test_signing.py
@@ -4,6 +4,7 @@ import pytest
 import requests_http_signature
 
 from funkwhale_api.federation import signing
+from funkwhale_api.federation import keys
 
 
 def test_can_sign_and_verify_request(factories):
@@ -45,7 +46,7 @@ def test_verify_fails_with_wrong_key(factories):
 
 
 def test_can_verify_django_request(factories, api_request):
-    private_key, public_key = signing.get_key_pair()
+    private_key, public_key = keys.get_key_pair()
     signed_request = factories['federation.SignedRequest'](
         auth__key=private_key
     )
@@ -61,7 +62,7 @@ def test_can_verify_django_request(factories, api_request):
 
 
 def test_can_verify_django_request_digest(factories, api_request):
-    private_key, public_key = signing.get_key_pair()
+    private_key, public_key = keys.get_key_pair()
     signed_request = factories['federation.SignedRequest'](
         auth__key=private_key,
         method='post',
@@ -81,7 +82,7 @@ def test_can_verify_django_request_digest(factories, api_request):
 
 
 def test_can_verify_django_request_digest_failure(factories, api_request):
-    private_key, public_key = signing.get_key_pair()
+    private_key, public_key = keys.get_key_pair()
     signed_request = factories['federation.SignedRequest'](
         auth__key=private_key,
         method='post',
@@ -102,7 +103,7 @@ def test_can_verify_django_request_digest_failure(factories, api_request):
 
 
 def test_can_verify_django_request_failure(factories, api_request):
-    private_key, public_key = signing.get_key_pair()
+    private_key, public_key = keys.get_key_pair()
     signed_request = factories['federation.SignedRequest'](
         auth__key=private_key
     )
diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..8bada65bb03c2b17bbc3428b3846a8c1a3f416b7
--- /dev/null
+++ b/api/tests/federation/test_utils.py
@@ -0,0 +1,14 @@
+import pytest
+
+from funkwhale_api.federation import utils
+
+
+@pytest.mark.parametrize('url,path,expected', [
+    ('http://test.com', '/hello', 'http://test.com/hello'),
+    ('http://test.com/', 'hello', 'http://test.com/hello'),
+    ('http://test.com/', '/hello', 'http://test.com/hello'),
+    ('http://test.com', 'hello', 'http://test.com/hello'),
+])
+def test_full_url(settings, url, path, expected):
+    settings.FUNKWHALE_URL = url
+    assert utils.full_url(path) == expected
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d8218c230293211f45b16594d32cdbdc131f2f4
--- /dev/null
+++ b/api/tests/federation/test_views.py
@@ -0,0 +1,69 @@
+from django.urls import reverse
+
+import pytest
+
+from funkwhale_api.federation import webfinger
+
+
+def test_instance_actor(db, settings, api_client):
+    settings.FUNKWHALE_URL = 'http://test.com'
+    url = reverse('federation:instance-actor')
+    response = api_client.get(url)
+    assert response.data['id'] == (
+      settings.FUNKWHALE_URL + url
+    )
+    assert response.data['type'] == 'Service'
+    assert response.data['inbox'] == (
+      settings.FUNKWHALE_URL + reverse('federation:instance-inbox')
+    )
+    assert response.data['outbox'] == (
+      settings.FUNKWHALE_URL + reverse('federation:instance-outbox')
+    )
+    assert response.data['@context'] == [
+      'https://www.w3.org/ns/activitystreams',
+      'https://w3id.org/security/v1',
+      {},
+    ]
+
+
+@pytest.mark.parametrize('route', [
+    'instance-outbox',
+    'instance-inbox',
+    'instance-actor',
+    'well-known-webfinger',
+])
+def test_instance_inbox_405_if_federation_disabled(
+        db, settings, api_client, route):
+    settings.FEDERATION_ENABLED = False
+    url = reverse('federation:{}'.format(route))
+    response = api_client.get(url)
+
+    assert response.status_code == 405
+
+
+def test_wellknown_webfinger_validates_resource(
+    db, api_client, settings, mocker):
+    clean = mocker.spy(webfinger, 'clean_resource')
+    settings.FEDERATION_ENABLED = True
+    url = reverse('federation:well-known-webfinger')
+    response = api_client.get(url, data={'resource': 'something'})
+
+    clean.assert_called_once_with('something')
+    assert url == '/.well-known/webfinger'
+    assert response.status_code == 400
+    assert response.data['errors']['resource'] == (
+        'Missing webfinger resource type'
+    )
+
+
+def test_wellknown_webfinger_system(
+    db, api_client, settings, mocker):
+    settings.FEDERATION_ENABLED = True
+    settings.FEDERATION_HOSTNAME = 'test.federation'
+    url = reverse('federation:well-known-webfinger')
+    response = api_client.get(
+        url, data={'resource': 'acct:service@test.federation'})
+
+    assert response.status_code == 200
+    assert response['Content-Type'] == 'application/jrd+json'
+    assert response.data == webfinger.serialize_system_acct()
diff --git a/api/tests/federation/test_webfinger.py b/api/tests/federation/test_webfinger.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cee9c8c71083798e8f0156140e0ca5303d358e3
--- /dev/null
+++ b/api/tests/federation/test_webfinger.py
@@ -0,0 +1,66 @@
+import pytest
+
+from django import forms
+from django.urls import reverse
+
+from funkwhale_api.federation import webfinger
+
+
+def test_webfinger_clean_resource():
+    t, r = webfinger.clean_resource('acct:service@test.federation')
+    assert t == 'acct'
+    assert r == 'service@test.federation'
+
+
+@pytest.mark.parametrize('resource,message', [
+    ('', 'Invalid resource string'),
+    ('service@test.com', 'Missing webfinger resource type'),
+    ('noop:service@test.com', 'Invalid webfinger resource type'),
+])
+def test_webfinger_clean_resource_errors(resource, message):
+    with pytest.raises(forms.ValidationError) as excinfo:
+        webfinger.clean_resource(resource)
+
+        assert message == str(excinfo)
+
+
+def test_webfinger_clean_acct(settings):
+    settings.FEDERATION_HOSTNAME = 'test.federation'
+    username, hostname = webfinger.clean_acct('service@test.federation')
+    assert username == 'service'
+    assert hostname == 'test.federation'
+
+
+@pytest.mark.parametrize('resource,message', [
+    ('service', 'Invalid format'),
+    ('service@test.com', 'Invalid hostname'),
+    ('noop@test.federation', 'Invalid account'),
+])
+def test_webfinger_clean_acct_errors(resource, message, settings):
+    settings.FEDERATION_HOSTNAME = 'test.federation'
+
+    with pytest.raises(forms.ValidationError) as excinfo:
+        webfinger.clean_resource(resource)
+
+        assert message == str(excinfo)
+
+
+def test_service_serializer(settings):
+    settings.FEDERATION_HOSTNAME = 'test.federation'
+    settings.FUNKWHALE_URL = 'https://test.federation'
+
+    expected = {
+        'subject': 'acct:service@test.federation',
+        'links': [
+            {
+                'rel': 'self',
+                'href': 'https://test.federation/instance/actor',
+                'type': 'application/activity+json',
+            }
+        ],
+        'aliases': [
+            'https://test.federation/instance/actor',
+        ]
+    }
+
+    assert expected == webfinger.serialize_system_acct()
diff --git a/deploy/funkwhale_proxy.conf b/deploy/funkwhale_proxy.conf
index 1b1dd0d20e951455eb999e17aacc1c45e6fed669..312986f43a0bd2a15169eea427d9a2f54dd0e7fb 100644
--- a/deploy/funkwhale_proxy.conf
+++ b/deploy/funkwhale_proxy.conf
@@ -3,8 +3,8 @@ proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
-proxy_set_header X-Forwarded-Host   $host:$server_port;
-proxy_set_header X-Forwarded-Port   $server_port;
+proxy_set_header X-Forwarded-Host $host:$server_port;
+proxy_set_header X-Forwarded-Port $server_port;
 proxy_redirect off;
 
 # websocket support
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 1c7b9ae8357d8222aba78e9c6dc34f45fbbb8ca4..1c304b4938892bf87ba50ab05f2394784b020248 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -62,6 +62,16 @@ server {
         proxy_pass   http://funkwhale-api/api/;
     }
 
+    location /federation/ {
+        include /etc/nginx/funkwhale_proxy.conf;
+        proxy_pass   http://funkwhale-api/federation/;
+    }
+
+    location /.well-known/webfinger {
+        include /etc/nginx/funkwhale_proxy.conf;
+        proxy_pass   http://funkwhale-api/.well-known/webfinger;
+    }
+
     location /media/ {
         alias /srv/funkwhale/data/media/;
     }
diff --git a/dev.yml b/dev.yml
index dd3a55ddcb2eba19927a13ab1ae4b3e307f907f2..126efa683e7fe3650b82c03f02862acbc18cf4be 100644
--- a/dev.yml
+++ b/dev.yml
@@ -1,27 +1,35 @@
-version: '2'
+version: '3'
 
 services:
-
   front:
     build: front
-    env_file: .env.dev
+    env_file:
+      - .env.dev
+      - .env
     environment:
       - "HOST=0.0.0.0"
+      - "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
     ports:
-      - "8080:8080"
+      - "${WEBPACK_DEVSERVER_PORT-8080}:${WEBPACK_DEVSERVER_PORT-8080}"
     volumes:
       - './front:/app'
 
   postgres:
-    env_file: .env.dev
+    env_file:
+      - .env.dev
+      - .env
     image: postgres
 
   redis:
-    env_file: .env.dev
+    env_file:
+      - .env.dev
+      - .env
     image: redis:3.0
 
   celeryworker:
-    env_file: .env.dev
+    env_file:
+      - .env.dev
+      - .env
     build:
       context: ./api
       dockerfile: docker/Dockerfile.test
@@ -36,12 +44,14 @@ services:
       - C_FORCE_ROOT=true
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
-      - "FUNKWHALE_URL=http://funkwhale.test"
+      - "FUNKWHALE_URL=http://localhost"
     volumes:
       - ./api:/app
       - ./data/music:/music
   api:
-    env_file: .env.dev
+    env_file:
+      - .env.dev
+      - .env
     build:
       context: ./api
       dockerfile: docker/Dockerfile.test
@@ -56,19 +66,26 @@ services:
       - "DJANGO_SECRET_KEY=dev"
       - "DATABASE_URL=postgresql://postgres@postgres/postgres"
       - "CACHE_URL=redis://redis:6379/0"
-      - "FUNKWHALE_URL=http://funkwhale.test"
+      - "FUNKWHALE_URL=http://localhost"
     links:
       - postgres
       - redis
 
   nginx:
-    env_file: .env.dev
+    command: /entrypoint.sh
+    env_file:
+      - .env.dev
+      - .env
     image: nginx
+    environment:
+      - "WEBPACK_DEVSERVER_PORT=${WEBPACK_DEVSERVER_PORT-8080}"
     links:
       - api
       - front
     volumes:
       - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
+      - ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro
+      - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf.template:ro
       - ./api/funkwhale_api/media:/protected/media
     ports:
       - "0.0.0.0:6001:6001"
diff --git a/docker/nginx/conf.dev b/docker/nginx/conf.dev
index 9847c2dcbcc71bf3040802d6289c7336ff40c340..e832a5ae3ee5b4d96833bd1cf5bf23fee6d5033f 100644
--- a/docker/nginx/conf.dev
+++ b/docker/nginx/conf.dev
@@ -37,19 +37,7 @@ http {
         listen 6001;
         charset     utf-8;
         client_max_body_size 20M;
-
-        # global proxy pass config
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-        proxy_set_header X-Forwarded-Host   localhost:8080;
-        proxy_set_header X-Forwarded-Port   8080;
-        proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection $connection_upgrade;
-        proxy_redirect off;
-
+        include /etc/nginx/funkwhale_proxy.conf;
         location /_protected/media {
             internal;
             alias   /protected/media;
@@ -63,8 +51,7 @@ http {
             if ($request_uri ~* "[^\?]+\?(.*)$") {
                 set $query $1;
             }
-            proxy_set_header X-Forwarded-Host   localhost:8080;
-            proxy_set_header X-Forwarded-Port   8080;
+            include /etc/nginx/funkwhale_proxy.conf;
             proxy_pass http://api:12081/api/v1/trackfiles/viewable/?$query;
             proxy_pass_request_body off;
             proxy_set_header        Content-Length "";
@@ -78,6 +65,7 @@ http {
             if ($args ~ (.*)jwt=[^&]*(.*)) {
                 set $cleaned_args $1$2;
             }
+            include /etc/nginx/funkwhale_proxy.conf;
             proxy_cache_key "$scheme$request_method$host$uri$is_args$cleaned_args";
             proxy_cache transcode;
             proxy_cache_valid 200 7d;
@@ -87,6 +75,7 @@ http {
             proxy_pass http://api:12081;
         }
         location / {
+            include /etc/nginx/funkwhale_proxy.conf;
             proxy_pass   http://api:12081/;
         }
     }
diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..93b4a0533fe4edd3a7ba815cf9a7b1d0ac118824
--- /dev/null
+++ b/docker/nginx/entrypoint.sh
@@ -0,0 +1,9 @@
+#!/bin/bash -eux
+
+echo "Copying template file..."
+cp /etc/nginx/funkwhale_proxy.conf{.template,}
+sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host localhost:${WEBPACK_DEVSERVER_PORT}/" /etc/nginx/funkwhale_proxy.conf
+sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${WEBPACK_DEVSERVER_PORT}/" /etc/nginx/funkwhale_proxy.conf
+
+cat /etc/nginx/funkwhale_proxy.conf
+nginx -g "daemon off;"
diff --git a/front/config/index.js b/front/config/index.js
index 14cbe3e4388ea6a5e18a6053a1611ffd0cd6ab38..925b4defe3fb89ca4879702c7e3eb5b4cdfbbabf 100644
--- a/front/config/index.js
+++ b/front/config/index.js
@@ -23,25 +23,37 @@ module.exports = {
   },
   dev: {
     env: require('./dev.env'),
-    port: 8080,
+    port: parseInt(process.env.WEBPACK_DEVSERVER_PORT),
     host: '127.0.0.1',
     autoOpenBrowser: true,
     assetsSubDirectory: 'static',
     assetsPublicPath: '/',
     proxyTable: {
-      '/api': {
+      '**': {
         target: 'http://nginx:6001',
         changeOrigin: true,
-        ws: true
+        ws: true,
+        filter: function (pathname, req) {
+          let proxified = ['.well-known', 'staticfiles', 'media', 'instance', 'api']
+          let matches = proxified.filter(e => {
+            return pathname.match(`^/${e}`)
+          })
+          return matches.length > 0
+        }
       },
-      '/media': {
-        target: 'http://nginx:6001',
-        changeOrigin: true,
-      },
-      '/staticfiles': {
-        target: 'http://nginx:6001',
-        changeOrigin: true,
-      }
+      // '/.well-known': {
+      //   target: 'http://nginx:6001',
+      //   changeOrigin: true
+      // },
+      // '/media': {
+      //   target: 'http://nginx:6001',
+      //   changeOrigin: true,
+      // },
+      // '/staticfiles': {
+      //   target: 'http://nginx:6001',
+      //   changeOrigin: true,
+      // },
+
     },
     // CSS Sourcemaps off by default because relative paths are "buggy"
     // with this option, according to the CSS-Loader README
diff --git a/front/package.json b/front/package.json
index 201694e43648e08c6bd23b2fa869ca83c29c7e2d..d67375f7e5851f7cf2af379ac298a6f85f973f78 100644
--- a/front/package.json
+++ b/front/package.json
@@ -41,7 +41,7 @@
     "autoprefixer": "^6.7.2",
     "babel-core": "^6.22.1",
     "babel-eslint": "^7.1.1",
-    "babel-loader": "^6.2.10",
+    "babel-loader": "7",
     "babel-plugin-istanbul": "^4.1.1",
     "babel-plugin-transform-runtime": "^6.22.0",
     "babel-preset-env": "^1.3.2",
@@ -101,7 +101,7 @@
     "vue-loader": "^12.1.0",
     "vue-style-loader": "^3.0.1",
     "vue-template-compiler": "^2.3.3",
-    "webpack": "^2.6.1",
+    "webpack": "3",
     "webpack-bundle-analyzer": "^2.2.1",
     "webpack-dev-middleware": "^1.10.0",
     "webpack-hot-middleware": "^2.18.0",