diff --git a/.gitignore b/.gitignore
index 8db764f4eadb6e11a9fc766cea5aa1a491b66aa2..f81b8cafd3ab9735e5ecaa21446bc13c736f0ecb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,7 @@ venv*
 celerybeat-*
 env.sh
 .cache
-.idea/
\ No newline at end of file
+.idea/
+docker-compose.yml
+.venv
+.envrc
diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py
index 181834137a187acd82058c00098166d5786c3710..217e885d909e50b65e3e42b0b3660b588c78cfe1 100644
--- a/helios_auth/auth_systems/__init__.py
+++ b/helios_auth/auth_systems/__init__.py
@@ -1,5 +1,5 @@
 from django.conf import settings
-from . import password, twitter, linkedin, cas, facebook, google, yahoo, clever, github
+from . import password, twitter, linkedin, cas, facebook, google, yahoo, clever, github, pirati
 
 AUTH_SYSTEMS = {}
 
@@ -12,6 +12,7 @@ AUTH_SYSTEMS['google'] = google
 AUTH_SYSTEMS['yahoo'] = yahoo
 AUTH_SYSTEMS['clever'] = clever
 AUTH_SYSTEMS['github'] = github
+AUTH_SYSTEMS['pirati'] = pirati
 
 # not ready
 #import live
diff --git a/helios_auth/auth_systems/pirati.py b/helios_auth/auth_systems/pirati.py
new file mode 100644
index 0000000000000000000000000000000000000000..80fc39ca4847d9bd714754e59e3aa6930f5f3f1e
--- /dev/null
+++ b/helios_auth/auth_systems/pirati.py
@@ -0,0 +1,283 @@
+"""
+Pirati Authentication
+"""
+
+import json
+import re
+from urllib import request
+
+from celery import shared_task
+from django.core.mail import send_mail
+from django.conf import settings
+from requests_oauthlib import OAuth2Session
+from unidecode import unidecode
+
+
+# some parameters to indicate that status updating is not possible
+STATUS_UPDATES = False
+
+# display tweaks
+LOGIN_MESSAGE = "Přihlásit se pirátskou identitou"
+
+
+PIRATI_ENDPOINT_URL = f"{settings.PIRATI_REALM_URL}/protocol/openid-connect/auth"
+PIRATI_TOKEN_URL = f"{settings.PIRATI_REALM_URL}/protocol/openid-connect/token"
+PIRATI_USERINFO_URL = f"{settings.PIRATI_REALM_URL}/protocol/openid-connect/userinfo"
+
+
+###############################################################################
+# Custom helper functions
+
+
+def call_octopus_api(query, variables=None):
+    payload = json.dumps({"query": query, "variables": variables or {}}).encode("utf-8")
+    req = request.Request(settings.OCTOPUS_API_URL, method="POST")
+    req.add_header("Content-Type", "application/json; charset=utf-8")
+    req.add_header("Authorization", f"Bearer {settings.OCTOPUS_API_TOKEN}")
+    response = request.urlopen(req, payload)
+    data = json.loads(response.read())
+    if "errors" in data:
+        raise RuntimeError(
+            f"API call failed!\n - query:\n----\n{query}\n----\n - variables:\n----\n{variables}\n----\n- response:\n----\n{data}\n----\n"
+        )
+    return data
+
+
+def get_octopus_person(username):
+    query = """
+    query data ($username: String!) {
+        allPeople (first: 1, filters: {username: {iExact: $username}}) {
+            edges {
+                node {
+                    username
+                    displayName
+                    email
+                }
+            }
+        }
+    }
+    """
+    variables = {"username": username}
+    data = call_octopus_api(query, variables)
+    return data["data"]["allPeople"]["edges"][0]["node"]
+
+
+def get_octopus_groups():
+    query = """
+    query {
+        allGroups (filters: {voting: true}) {
+            edges {
+                node {
+                    id
+                    name
+                }
+            }
+        }
+    }
+    """
+    data = call_octopus_api(query)
+    return [edge["node"] for edge in data["data"]["allGroups"]["edges"]]
+
+
+def get_octopus_group(group_id):
+    query = """
+    query data ($id: GlobalID!) {
+        group (id: $id) {
+            id
+            name
+        }
+    }
+    """
+    variables = {"id": group_id}
+    data = call_octopus_api(query, variables)
+    return data["data"]["group"]
+
+
+def get_octopus_group_members(group_id):
+    query = """
+    query data ($id: GlobalID!) {
+        group (id: $id) {
+            id
+            memberships {
+                person {
+                    username
+                    displayName
+                    email
+                }
+            }
+        }
+    }
+    """
+    variables = {"id": group_id}
+    data = call_octopus_api(query, variables)
+    return [m["person"] for m in data["data"]["group"]["memberships"]]
+
+
+def group_sorter(group):
+    name = unidecode(group["name"])
+    if name == "Celostatni forum":
+        return f"0_{name}"
+
+    if re.match(r"^K[FS] ", name):
+        return f"1_{name[3:]}"
+
+    if re.match(r"^M[FS] Ch", name):
+        return f"3_{name[3:]}"
+    if re.match(r"^M[FS] [ABCDEFGH]", name):
+        return f"2_{name[3:]}"
+    if re.match(r"^M[FS] ", name):
+        return f"4_{name[3:]}"
+
+    return f"9_{name}"
+
+
+def is_old_group(group_id):
+    try:
+        int(group_id)
+        return True
+    except:
+        pass
+
+    if group_id.startswith("deadbeef-babe-"):
+        return True
+
+    return False
+
+
+def person_to_user_info(person):
+    return {
+        "type": "pirati",
+        "id": person["username"].lower(),
+        "name": person["displayName"],
+        "info": {"email": person["email"], "name": person["displayName"]},
+        "token": {},
+    }
+
+
+def old_member_to_user_info(member):
+    return {
+        "type": "pirati",
+        "id": member["username"].lower(),
+        "name": member["username"],
+        "info": {"email": member["email"], "name": member["username"]},
+        "token": {},
+    }
+
+
+###############################################################################
+# Celery tasks
+
+
+@shared_task(
+    autoretry_for=(Exception,),
+    max_retries=160,  # 3 days
+    retry_backoff=True,
+    retry_backoff_max=1800,
+)
+def send_email_task(recipient, subject, body):
+    send_mail(subject, body, settings.SERVER_EMAIL, [recipient], fail_silently=False)
+
+
+###############################################################################
+# Helios stuff
+
+can_list_category_members = True
+
+
+def get_auth_url(request, redirect_url):
+    request.session["pirate_redirect_url"] = redirect_url
+    oauth = OAuth2Session(settings.PIRATI_CLIENT_ID, redirect_uri=redirect_url)
+    url, state = oauth.authorization_url(PIRATI_ENDPOINT_URL)
+    return url
+
+
+def get_user_info_after_auth(request):
+    oauth = OAuth2Session(
+        settings.PIRATI_CLIENT_ID, redirect_uri=request.session["pirate_redirect_url"]
+    )
+    token = oauth.fetch_token(
+        PIRATI_TOKEN_URL,
+        client_secret=settings.PIRATI_CLIENT_SECRET,
+        code=request.GET["code"],
+    )
+    response = oauth.get(PIRATI_USERINFO_URL)
+    data = response.json()
+    person = get_octopus_person(data["preferred_username"])
+    info = person_to_user_info(person)
+    info["user_id"] = info.pop("id")
+    return info
+
+
+def do_logout(user):
+    return None
+
+
+def update_status(token, message):
+    pass
+
+
+def send_message(user_id, user_name, user_info, subject, body):
+    recipient = "%s <%s>" % (user_info.get("name", user_name), user_info["email"])
+    send_email_task.delay(recipient, subject, body)
+
+
+def generate_constraint(category_id, user):
+    return category_id
+
+
+def eligibility_category_id(constraint):
+    return constraint
+
+
+def check_constraint(constraint, user):
+    if is_old_group(constraint):
+        userinfo = json.load(
+            request.urlopen("https://graph.pirati.cz/user/" + user.user_id)
+        )
+        id = userinfo["id"]
+        usergroups = json.load(
+            request.urlopen("https://graph.pirati.cz/" + id + "/groups")
+        )
+        for usergroup in usergroups:
+            if usergroup["id"] == constraint:
+                return True
+    else:
+        people = get_octopus_group_members(constraint)
+        usernames = [person["username"].lower() for person in people]
+        if user.user_id in usernames:
+            return True
+    return False
+
+
+def list_categories(user):
+    return sorted(get_octopus_groups(), key=group_sorter)
+
+
+def list_category_members(category_id):
+    if is_old_group(category_id):
+        members = json.load(
+            request.urlopen("https://graph.pirati.cz/" + category_id + "/members")
+        )
+        return [old_member_to_user_info(member) for member in members]
+    else:
+        people = get_octopus_group_members(category_id)
+        return [person_to_user_info(person) for person in people]
+
+
+def pretty_eligibility(constraint):
+    if is_old_group(constraint):
+        group = json.load(request.urlopen("https://graph.pirati.cz/" + constraint))
+        name = group["username"]
+    else:
+        group = get_octopus_group(constraint)
+        name = group["name"]
+    return f"Osoby ve skupině „{name}“"
+
+
+#
+# Election Creation
+#
+
+
+def can_create_election(user_id, user_info):
+    return True
diff --git a/helios_auth/media/login-icons/pirati.png b/helios_auth/media/login-icons/pirati.png
new file mode 100755
index 0000000000000000000000000000000000000000..1c22646e755206601bf1690c9edb6d455665b688
Binary files /dev/null and b/helios_auth/media/login-icons/pirati.png differ
diff --git a/requirements.txt b/requirements.txt
index c718cf60eca22ec4204ade3de8e93f8096a97819..f5e6b05f0027523fa10070481e48267e93354532 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,3 +22,6 @@ boto==2.49.0
 django-ses==0.8.14
 oauth2client==4.1.3
 rollbar==0.14.7
+
+requests-oauthlib==1.3.1
+unidecode==1.3.6
diff --git a/settings.py b/settings.py
index 20d2330644add7a4cedf0cdb11c293efe3bf517e..a7f24a3e5a409ad5b60568776d0ad8da6f8f80f7 100644
--- a/settings.py
+++ b/settings.py
@@ -14,9 +14,9 @@ def get_from_env(var, default):
     else:
         return default
 
-DEBUG = (get_from_env('DEBUG', '1') == '1')
+DEBUG = (get_from_env('DEBUG', '0') == '1')
 
-# add admins of the form: 
+# add admins of the form:
 #    ('Ben Adida', 'ben@adida.net'),
 # if you want to be emailed about errors.
 admin_email = get_from_env('ADMIN_EMAIL', None)
@@ -212,7 +212,7 @@ HELP_EMAIL_ADDRESS = get_from_env('HELP_EMAIL_ADDRESS', 'help@heliosvoting.org')
 AUTH_TEMPLATE_BASE = "server_ui/templates/base.html"
 HELIOS_TEMPLATE_BASE = "server_ui/templates/base.html"
 HELIOS_ADMIN_ONLY = False
-HELIOS_VOTERS_UPLOAD = True
+HELIOS_VOTERS_UPLOAD = (get_from_env('HELIOS_VOTERS_UPLOAD', 1) == 1)
 HELIOS_VOTERS_EMAIL = True
 
 # are elections private by default?
@@ -221,9 +221,9 @@ HELIOS_PRIVATE_DEFAULT = False
 # authentication systems enabled
 # AUTH_ENABLED_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo']
 AUTH_ENABLED_SYSTEMS = get_from_env('AUTH_ENABLED_SYSTEMS',
-                                    get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'password,google,facebook')
+                                    get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'pirati')
                                     ).split(",")
-AUTH_DEFAULT_SYSTEM = get_from_env('AUTH_DEFAULT_SYSTEM', get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None))
+AUTH_DEFAULT_SYSTEM = get_from_env('AUTH_DEFAULT_SYSTEM', get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', 'pirati'))
 
 # google
 GOOGLE_CLIENT_ID = get_from_env('GOOGLE_CLIENT_ID', '')
@@ -295,5 +295,16 @@ if ROLLBAR_ACCESS_TOKEN:
   MIDDLEWARE += ['rollbar.contrib.django.middleware.RollbarNotifierMiddleware',]
   ROLLBAR = {
     'access_token': ROLLBAR_ACCESS_TOKEN,
-    'environment': 'development' if DEBUG else 'production',  
+    'environment': 'development' if DEBUG else 'production',
   }
+
+# auth setup
+PIRATI_REALM_URL = get_from_env('PIRATI_REALM_URL', '')
+PIRATI_CLIENT_ID = get_from_env('PIRATI_CLIENT_ID', '')
+PIRATI_CLIENT_SECRET = get_from_env('PIRATI_CLIENT_SECRET', '')
+
+OCTOPUS_API_URL = get_from_env('OCTOPUS_API_URL', '')
+OCTOPUS_API_TOKEN = get_from_env('OCTOPUS_API_TOKEN', '')
+
+if DEBUG:
+    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'