diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..74af99f50ed3c6a5e73ee55326e8fa582c8cda67
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,32 @@
+stages:
+  - unit-test
+  - deploy
+  - functional-test
+
+FunctionalTest:
+  image: python:3.7
+  stage: functional-test
+  before_script:
+    - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
+    - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
+    - pip install -r requirements.txt
+    - apt-get update -qq && apt-get install -y -qq unzip
+    - apt-get install -y google-chrome-stable
+    - apt-get install -y xvfb
+    - wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip
+    - unzip chromedriver_linux64.zip
+
+  when: on_success
+  script:
+    - python3 manage.py test functional_tests
+
+UnitTest:
+  image: python:3.7
+  stage: unit-test
+  before_script:
+    - pip3 install --upgrade pip
+    - pip3 install -r requirements.txt
+    - python3 manage.py muttest lists --modules lists.views
+  when: on_success
+  script:
+    - python3 manage.py test lists
diff --git a/accounts/__init__.py b/accounts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/accounts/admin.py b/accounts/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e
--- /dev/null
+++ b/accounts/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/accounts/apps.py b/accounts/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b3fc5a44939430bfb326ca9a33f80e99b06b5be
--- /dev/null
+++ b/accounts/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+    name = 'accounts'
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..0919bd1bc59beb522fff796724531ad9bbef15d8
--- /dev/null
+++ b/accounts/migrations/0001_initial.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.1.7 on 2019-11-12 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='User',
+            fields=[
+                ('email', models.EmailField(max_length=254, primary_key=True, serialize=False)),
+            ],
+        ),
+    ]
diff --git a/accounts/migrations/0002_token.py b/accounts/migrations/0002_token.py
new file mode 100644
index 0000000000000000000000000000000000000000..de6895d74aa8f04c99032c6ea68b892e667f4048
--- /dev/null
+++ b/accounts/migrations/0002_token.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.1.7 on 2019-11-12 13:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('accounts', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Token',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('email', models.EmailField(max_length=254)),
+                ('uid', models.CharField(max_length=255)),
+            ],
+        ),
+    ]
diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/accounts/models.py b/accounts/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..ddd7ffe0276b74c64e835fe1a0e6c1d36283ce3d
--- /dev/null
+++ b/accounts/models.py
@@ -0,0 +1,16 @@
+from django.db import models
+
+import uuid
+
+
+class User(models.Model):
+    email = models.EmailField(primary_key=True)
+    REQUIRED_FIELDS = []
+    USERNAME_FIELD = 'email'
+    is_anonymous = False
+    is_authenticated = True
+
+
+class Token(models.Model):
+    email = models.EmailField()
+    uid = models.CharField(default=uuid.uuid4, max_length=40)
diff --git a/accounts/tests/__init__.py b/accounts/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/accounts/tests/test_models.py b/accounts/tests/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba08223cf3366fd4e9f4672b1d578015cf7c89c8
--- /dev/null
+++ b/accounts/tests/test_models.py
@@ -0,0 +1,25 @@
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+
+from accounts.models import Token
+
+User = get_user_model()
+
+
+class UserModelTest(TestCase):
+
+    def test_user_is_valid_with_email_only(self):
+        user = User(email='a@b.com')
+        user.full_clean()  # should not raise
+
+    def test_email_is_primary_key(self):
+        user = User(email='a@b.com')
+        self.assertEqual(user.pk, 'a@b.com')
+
+
+class TokenModelTest(TestCase):
+
+    def test_links_user_with_auto_generated_uid(self):
+        token1 = Token.objects.create(email='a@b.com')
+        token2 = Token.objects.create(email='a@b.com')
+        self.assertNotEqual(token1.uid, token2.uid)
diff --git a/accounts/views.py b/accounts/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e
--- /dev/null
+++ b/accounts/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.