diff --git a/config.py.example b/config.py.example
new file mode 100644
index 0000000000000000000000000000000000000000..4d8d389a2785d2541fa4b2c5f41c524d65af04d7
--- /dev/null
+++ b/config.py.example
@@ -0,0 +1,18 @@
+SQLALCHEMY_DATABASE_URI = "postgresql://proto3:@/proto3"
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+SECRET_KEY = "abc"
+DEBUG = False
+MAIL_ACTIVE = True
+MAIL_FROM = "protokolle@example.com"
+MAIL_HOST = "mail.example.com:465"
+MAIL_USER = "user"
+MAIL_PASSWORD = "password"
+MAIL_PREFIX = "protokolle"
+CELERY_BROKER_URL = "redis://localhost:6379/0"
+CELERY_TASK_SERIALIZER = "pickle"
+CELERY_ACCEPT_CONTENT = ["pickle"]
+URL_ROOT = "protokolle.example.com"
+URL_PROTO = "https"
+URL_PATH = "/"
+URL_PARAMS = ""
+ERROR_CONTEXT_LINES = 3
diff --git a/models/database.py b/models/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..9479ca042f4b306e8cf9cdd1c9b569c1961bb11b
--- /dev/null
+++ b/models/database.py
@@ -0,0 +1,219 @@
+from flask import render_template, send_file, url_for, redirect, flash, request
+
+from datetime import datetime, date, timedelta
+import time
+import math
+
+from shared import db
+from utils import random_string, url_manager
+
+#from models.tables import TexResponsiblesTable, TexSupportersTable
+
+from sqlalchemy.orm import relationship, backref
+
+class ProtocolType(db.Model):
+    __tablename__ = "protocoltypes"
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String, unique=True)
+    short_name = db.Column(db.String, unique=True)
+    organization = db.Column(db.String)
+    is_public = db.Column(db.Boolean)
+    private_group = db.Column(db.String)
+    public_group = db.Column(db.String)
+    private_mail = db.Column(db.String)
+    public_mail = db.Column(db.String)
+
+    protocols = relationship("Protocol", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="Protocol.id")
+    default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number")
+    reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.time_before")
+
+    def __init__(self, name, short_name, organization,
+        is_public, private_group, public_group, private_mail, public_mail):
+        self.name = name
+        self.short_name = short_name
+        self.organization = organization
+        self.is_public = is_public
+        self.private_group = private_group
+        self.public_group = public_group
+        self.private_mail = private_mail
+        self.public_mail = public_mail
+
+    def __repr__(self):
+        return "<ProtocolType(id={}, short_name={}, name={}, organization={})>".format(
+            self.id, self.short_name, self.name, self.organization)
+
+class Protocol(db.Model):
+    __tablename__ = "protocols"
+    id = db.Column(db.Integer, primary_key=True)
+    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
+    source = db.Column(db.String, nullable=True)
+    date = db.Column(db.Date)
+    start_time = db.Column(db.Time)
+    end_time = db.Column(db.Time)
+    author = db.Column(db.String)
+    participants = db.Column(db.String)
+    location = db.Column(db.String)
+
+    tops = relationship("TOP", backref=backref("protocol"), cascade="all, delete-orphan", order_by="TOP.number")
+    decisions = relationship("Decision", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Decision.id")
+    documents = relationship("Document", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Document.is_compiled")
+    errors = relationship("Error", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Error.id")
+
+    def __init__(self, protocoltype_id, date, source=None, start_time=None, end_time=None, author=None, participants=None, location=None):
+        self.protocoltype_id = protocoltype_id
+        self.date = date
+        self.source = source
+        self.start_time = start_time
+        self.end_time = end_time
+        self.author = author
+        self.participants = participants
+        self.location = location
+
+    def __repr__(self):
+        return "<Protocol(id={}, protocoltype_id={})>".format(
+            self.id, self.protocoltype_id)
+
+    def create_error(self, action, name, description):
+        now = datetime.now()
+        return Error(self.id, action, name, now, description)
+
+    def fill_from_remarks(self, remarks):
+        self.date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y")
+        self.start_time = time.strptime(remarks["Beginn"].value, "%H:%M")
+        self.end_time = time.strptime(remarks["Ende"].value, "%H:%M")
+        self.author = remarks["Autor"].value
+        self.participants = remarks["Anwesende"].value
+        self.location = remarks["Ort"].value
+
+
+class DefaultTOP(db.Model):
+    __tablename__ = "defaulttops"
+    id = db.Column(db.Integer, primary_key=True)
+    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
+    name = db.Column(db.String)
+    number = db.Column(db.Integer)
+
+    def __init__(self, protocoltype_id, name, number):
+        self.protocoltype_id = protocoltype_id
+        self.name = name
+        self.number = number
+
+    def __repr__(self):
+        return "<DefaultTOP(id={}, protocoltype_id={}, name={}, number={})>".format(
+            self.id, self.protocoltype_id, self.name, self.number)
+
+    def is_at_end(self):
+        return self.number < 0
+
+class TOP(db.Model):
+    __tablename__ = "tops"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    name = db.Column(db.String)
+    number = db.Column(db.String)
+    planned = db.Column(db.Boolean)
+
+    def __init__(self, protocol_id, name, number, planned):
+        self.protocol_id = protocol_id
+        self.name = name
+        self.number = number
+        self.planned = planned
+
+    def __repr__(self):
+        return "<TOP(id={}, protocol_id={}, name={}, number={}, planned={})>".format(
+            self.id, self.protocol_id, self.name, self.number, self.planned)
+
+class Document(db.Model):
+    __tablename__ = "documents"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    name = db.Column(db.String)
+    filename = db.Column(db.String, unique=True)
+    is_compiled = db.Column(db.Boolean)
+
+    def __init__(self, protocol_id, name, filename, is_compiled):
+        self.protocol_id = protocol_id
+        self.name = name
+        self.filename = filename
+        self.is_compiled = is_compiled
+
+    def __repr__(self):
+        return "<Document(id={}, protocol_id={}, name={}, filename={}, is_compiled={})>".format(
+            self.id, self.protocol_id, self.name, self.filename, self.is_compiled)
+
+class Todo(db.Model):
+    __tablename__ = "todos"
+    id = db.Column(db.Integer, primary_key=True)
+    who = db.Column(db.String)
+    description = db.Column(db.String)
+    tags = db.Column(db.String)
+    done = db.Column(db.Boolean)
+
+    protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos")
+
+    def __init__(self, who, description, tags, done):
+        self.who = who
+        self.description = description
+        self.tags = tags
+        self.done = done
+
+    def __repr__(self):
+        return "<Todo(id={}, who={}, description={}, tags={}, done={})>".format(
+            self.id, self.who, self.description, self.tags, self.done)
+
+class TodoProtocolAssociation(db.Model):
+    __tablename__ = "todoprotocolassociations"
+    todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"), primary_key=True)
+
+class Decision(db.Model):
+    __tablename__ = "decisions"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    content = db.Column(db.String)
+
+    def __init__(self, protocol_id, content):
+        self.protocol_id = protocol_id
+        self.content = content
+
+    def __repr__(self):
+        return "<Decision(id={}, protocol_id={}, content='{}')>".format(
+            self.id, self.protocol_id, self.content)
+
+class MeetingReminder(db.Model):
+    __tablename__ = "meetingreminders"
+    id = db.Column(db.Integer, primary_key=True)
+    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
+    time_before = db.Column(db.Interval)
+    send_public = db.Column(db.Boolean)
+    send_private = db.Column(db.Boolean)
+
+    def __init__(self, protocoltype_id, time_before, send_public, send_private):
+        self.protocoltype_id = protocoltype_id
+        self.time_before = time_before
+        self.send_public = send_public
+        self.send_private = send_private
+
+    def __repr__(self):
+        return "<MeetingReminder(id={}, protocoltype_id={}, time_before={}, send_public={}, send_private={})>".format(
+            self.id, self.protocoltype_id, self.time_before, self.send_public, self.send_private)
+
+class Error(db.Model):
+    __tablename__ = "errors"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    action = db.Column(db.String)
+    name = db.Column(db.String)
+    datetime = db.Column(db.DateTime)
+    description = db.Column(db.String)
+
+    def __init__(self, protocol_id, action, name, datetime, description):
+        self.protocol_id = protocol_id
+        self.action = action
+        self.name = name
+        self.datetime = datetime
+        self.description = description
+
+    def __repr__(self):
+        return "<Error(id={}, protocol_id={}, action={}, name={}, datetime={})>".format(
+            self.id, self.protocol_id, self.action, self.name, self.datetime)
diff --git a/parser.py b/parser.py
index e450c5e6495718bfb059b44b2beeb05ab23cd5c7..26e2a13ddb44991662e04566d3bb77ea67860b87 100644
--- a/parser.py
+++ b/parser.py
@@ -100,6 +100,10 @@ class Content(Element):
         for child in self.children:
             child.dump(level + 1)
 
+    def get_tags(self, tags):
+        tags.extend([child for child in self.children if isinstance(child, Tag)])
+        return tags
+
     @staticmethod
     def parse(match, current, linenumber=None):
         linenumber = Element.parse_inner(match, current, linenumber)
@@ -254,6 +258,13 @@ class Fork(Element):
             + "\n".join(map(lambda e: r"\item {}".format(e.render()), self.children)) + "\n"
             + r"\end{itemize}" + "\n")
 
+    def get_tags(self, tags=None):
+        if tags is None:
+            tags = []
+        for child in self.children:
+            child.get_tags(tags)
+        return tags
+
     def is_anonymous(self):
         return self.environment == None
 
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ea72be555d893bfa30ec83484869df860093bf73
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,20 @@
+alembic
+blinker
+Flask
+Flask-Migrate
+Flask-Script
+Flask-SQLAlchemy
+Flask-WTF
+Flask-SocketIO
+Jinja2
+Mako
+MarkupSafe
+psycopg2
+python-editor
+SQLAlchemy
+Werkzeug
+wheel
+WTForms
+celery[redis]
+regex
+watchdog
diff --git a/server.py b/server.py
new file mode 100755
index 0000000000000000000000000000000000000000..54e202d183db1b8ef4b2d6b57ac4bdc0f6544c24
--- /dev/null
+++ b/server.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+import locale
+locale.setlocale(locale.LC_TIME, "de_DE.utf8")
+
+from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response
+from flask_script import Manager, prompt
+from flask_migrate import Migrate, MigrateCommand
+#from flask_socketio import SocketIO
+from celery import Celery
+
+import config
+from shared import db, date_filter, datetime_filter
+from utils import is_past, mail_manager, url_manager
+
+app = Flask(__name__)
+app.config.from_object(config)
+db.init_app(app)
+migrate = Migrate(app, db)
+manager = Manager(app)
+manager.add_command("db", MigrateCommand)
+
+def make_celery(app, config):
+    celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
+    celery.conf.update(app.config)
+    return celery
+celery = make_celery(app, config)
+
+#def make_socketio(app, config):
+#    socketio = SocketIO(app)
+#    return socketio
+#socketio = make_socketio(app, config)
+
+app.jinja_env.trim_blocks = True
+app.jinja_env.lstrip_blocks = True
+app.jinja_env.filters["datify"] = date_filter
+app.jinja_env.filters["datetimify"] = datetime_filter
+app.jinja_env.filters["url_complete"] = url_manager.complete
+
+import tasks
+
+# blueprints here
+
+@app.route("/")
+def index():
+    return render_template("index.html")
+
+@app.route("/imprint/")
+def imprint():
+    return render_template("imprint.html")
+
+@app.route("/contact/")
+def contact():
+    return render_template("contact.html")
+
+@app.route("/privacy/")
+def privacy():
+    return render_template("privacy.html")
+
+if __name__ == "__main__":
+    manager.run()
diff --git a/shared.py b/shared.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3d79364de14a3a8e18fa8d89aa672be008df95e
--- /dev/null
+++ b/shared.py
@@ -0,0 +1,73 @@
+from flask_sqlalchemy import SQLAlchemy
+
+import re
+
+import config
+
+db = SQLAlchemy()
+
+# the following code is written by Lars Beckers and not to be published without permission
+latex_chars = [
+    ("\\", "\\backslash"), # this needs to be first
+    ("$", "\$"),
+    ('%', '\\%'),
+    ('&', '\\&'),
+    ('#', '\\#'),
+    ('_', '\\_'),
+    ('{', '\\{'),
+    ('}', '\\}'),
+    #('[', '\\['),
+    #(']', '\\]'),
+    #('"', '"\''),
+    ('~', '$\\sim{}$'),
+    ('^', '\\textasciicircum{}'),
+    ('Ë„', '\\textasciicircum{}'),
+    ('`', '{}`'),
+    ('-->', '$\longrightarrow$'),
+    ('->', '$\rightarrow$'),
+    ('==>', '$\Longrightarrow$'),
+    ('=>', '$\Rightarrow$'),
+    ('>=', '$\geq$'),
+    ('=<', '$\leq$'),
+    ('<', '$<$'),
+    ('>', '$>$'),
+    ('\\backslashin', '$\\in$'),
+    ('\\backslash', '$\\backslash$') # this needs to be last
+]
+
+def escape_tex(text):
+    out = text
+    for old, new in latex_chars:
+        out = out.replace(old, new)
+    # beware, the following is carefully crafted code
+    res = ''
+    k, l = (0, -1)
+    while k >= 0:
+        k = out.find('"', l+1)
+        if k >= 0:
+            res += out[l+1:k]
+            l = out.find('"', k+1)
+            if l >= 0:
+                res += '\\enquote{' + out[k+1:l] + '}'
+            else:
+                res += '"\'' + out[k+1:]
+            k = l
+        else:
+            res += out[l+1:]
+    # yes, this is not quite escaping latex chars, but anyway...
+    res = re.sub('([a-z])\(', '\\1 (', res)
+    res = re.sub('\)([a-z])', ') \\1', res)
+    #logging.debug('escape latex ({0}/{1}): {2} --> {3}'.format(len(text), len(res), text.split('\n')[0], res.split('\n')[0]))
+    return res
+
+def unhyphen(text):
+    return " ".join([r"\mbox{" + word + "}" for word in text.split(" ")])
+
+def date_filter(date):
+    return date.strftime("%d. %B %Y")
+def datetime_filter(date):
+    return date.strftime("%d. %B %Y, %H:%M")
+def date_filter_long(date):
+    return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W")
+def time_filter(time):
+    return time.strftime("%H:%m")
diff --git a/start_celery.sh b/start_celery.sh
new file mode 100755
index 0000000000000000000000000000000000000000..397bab8183fb1252615b6c0a965862f24bcdbfe1
--- /dev/null
+++ b/start_celery.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+celery -A server.celery worker --loglevel=debug --concurrency=4
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..c498188af7871246c813cf82381a1fc18e61c337
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,186 @@
+body {
+    font-family: sans-serif;
+}
+
+.header {
+    border-bottom: 1px solid black;
+    padding: 4px;
+    font-size: 16pt;
+    margin-bottom: 1pc;
+}
+
+.header > p {
+    display: inline;
+}
+
+.header-title {
+    font-weight: bold;
+}
+
+.header-link {
+    
+}
+
+.footer {
+    border-top: 1px solid black;
+    padding: 4px;
+    font-size: 10pt;
+    margin-top: 1pc;
+    width: 100%;
+}
+
+.footer > p {
+    display: inline;
+}
+
+.footer-text {
+}
+
+.footer-link {
+}
+
+.main {
+    max-width: 940px;
+    margin: 0px auto;
+}
+
+.main > p {
+    margin: 0px;
+    padding: 0.5ex;
+    display: inline-block;
+}
+
+.title {
+    font-size: 16pt;
+}
+
+.form {
+    background-color: #eee;
+    padding: 1ex;
+    border: 1px dashed gray;
+}
+
+.form-title {
+    font-size: 16pt;
+    margin-bottom: 3pt;
+    border-bottom: 1px solid black;
+}
+
+.form-field {
+    font-size: 12pt;
+    margin: 2pt;
+}
+
+.form-label {
+    font-size: 12pt;
+    display: inline-block;
+    min-width: 100px;
+}
+
+.form-textareafield > textarea {
+    width: 50%;
+    min-width: 350px;
+    max-width: 100%;
+    height: 150px;
+}
+
+.form-errors {
+    font-size: 10pt;
+    color: darkred;
+}
+
+.form-errors-entry {
+    margin: 0px;
+}
+
+.section {
+    margin-top: 1ex;
+    margin-bottom: 1ex;
+    background-color: #eee;
+    padding: 1ex;
+    border: 1px dashed gray;
+}
+
+.section > p {
+    display: inline;
+}
+
+.section-title {
+    font-weight: bold;
+    font-size: 14pt;
+}
+
+.table {
+    border-spacing: 0px;
+    border-collapse: collapse;
+    text-align: center;
+}
+
+.table-header {
+    font-size: 13pt;
+}
+
+.table-body > tr > td {
+    border-right: 1px solid gray;
+    border-left: 1px solid gray;
+    padding-left: 5px;
+    padding-right: 5px;
+}
+
+.table-body > tr:first-child {
+    border-top: 1px solid gray;
+}
+
+.table-body > tr:last-child {
+    border-bottom: 1px solid gray;
+}
+
+.table-body > tr:nth-child(odd) {
+    background-color: #fefefe;
+}
+
+.table-body > tr:nth-child(even) {
+    background-color: #f0f0f0;
+}
+
+.alert {
+    font-weight: bold;
+    text-align: center;
+}
+
+.alert-error {
+    color: #800000;
+}
+
+.alert-success {
+    color: #008000;
+}
+
+.alert-warning {
+    color: #808000;
+}
+
+.mail-content {
+    white-space: pre;
+}
+
+.imprint-header {
+    font-weight: bold;
+}
+
+.imprint > div {
+    
+}
+
+.privacy-title {
+    font-weight: bold;
+    font-size: 14pt;
+}
+
+.privacy-content {
+    
+}
+
+.pad-content {
+    width: 100%;
+}
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..adee236f1cc921815488329beaf3e4a9b3297254
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,215 @@
+from flask import render_template
+
+import os
+import subprocess
+import shutil
+
+from models.database import Document, Protocol, Error, Todo, Decision
+from server import celery, app
+from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, time_filter
+from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs
+from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork
+
+import config
+
+texenv = app.create_jinja_environment()
+texenv.block_start_string = r"\ENV{"
+texenv.block_end_string = r"}"
+texenv.variable_start_string = r"\VAR{"
+texenv.variable_end_string = r"}"
+texenv.comment_start_string = r"\COMMENT{"
+texenv.comment_end_string = r"}"
+texenv.filters["escape_tex"] = escape_tex
+texenv.filters["unhyphen"] = unhyphen
+texenv.trim_blocks = True
+texenv.lstrip_blocks = True
+texenv.filters["url_complete"] = url_manager.complete
+texenv.filters["datify"] = date_filter
+texenv.filters["datify_long"] = date_filter_long
+texenv.filters["datetimify"] = datetime_filter
+texenv.filters["timify"] = time_filter
+
+mailenv = app.create_jinja_environment()
+mailenv.trim_blocks = True
+mailenv.lstrip_blocks = True
+mailenv.filters["url_complete"] = url_manager.complete
+mailenv.filters["datify"] = date_filter
+mailenv.filters["datetimify"] = datetime_filter
+
+ID_FIELD_BEGINNING = "id "
+
+def parse_protocol(protocol, **kwargs):
+    parse_protocol_async.delay(protocol.id, encode_kwargs(kwargs))
+
+@celery.task
+def parse_protocol_async(protocol_id, encoded_kwargs):
+    with app.app_context():
+        with app.test_request_context("/"):
+            kwargs = decode_kwargs(encoded_kwargs)
+            protocol = Protocol.query.filter_by(id=protocol_id).first()
+            if protocol is None:
+                raise Exception("No protocol given. Aborting parsing.")
+            if protocol.source is None:
+                error = protocol.create_error("Parsing", "Protocol source is None", "")
+                db.session.add(error)
+                db.session.commit()
+                return
+            tree = None
+            try:
+                tree = parse(protocol.source)
+            except ParserException as exc:
+                context = ""
+                if exc.linenumber is not None:
+                    source_lines = source.splitlines()
+                    start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES)
+                    end_index = min(len(source_lines) - 1, exc.linenumber + config.ERROR_CONTEXT_LINES)
+                    context = "\n".join(source_lines[start_index:end_index])
+                error = protocol.create_error("Parsing", str(exc), context)
+                db.session.add(error)
+                db.session.commit()
+                return
+            remarks = {element.name: element for element in tree.children is isinstance(element, Remark)}
+            required_fields = ["Datum", "Anwesende", "Beginn", "Ende", "Autor", "Ort"]
+            missing_fields = [field for field in required_files if field not in remarks]
+            if len(missing_fields) > 0:
+                error = protocol.create_error("Parsing", "Missing fields", ", ".join(missing_fields))
+                db.session.add(error)
+                db.session.commit()
+                return
+            try:
+                protocol.fill_from_remarks(remarks)
+            except ValueError:
+                error = protocol.create_error(
+                    "Parsing", "Invalid fields",
+                    "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', "
+                    "but rather {}".format(
+                    ", ".join([remarks["Datum"], remarks["Beginn"], remarks["Ende"]])))
+                db.session.add(error)
+                db.session.commit()
+                return
+            old_todos = list(protocol.todos)
+            for todo in old_todos
+                protocol.todos.remove(todo)
+            db.session.commit()
+            tags = tree.get_tags()
+            todo_tags = [tag for tag in tags if tag.name == "todo"]
+            for todo_tag in todo_tags:
+                if len(todo_tag.values) < 2:
+                    error = protocol.create_error("Parsing", "Invalid todo-tag",
+                        "The todo tag in line {} needs at least "
+                        "information on who and what, "
+                        "but has less than that.".format(todo_tag.linenumber))
+                    db.session.add(error)
+                    db.session.commit()
+                    return
+                who = todo_tag.values[0]
+                what = todo_tag.values[1]
+                todo = None
+                for other_field in todo_tag.values[2:]:
+                    if other_field.startswith(ID_FIELD_BEGINNING):
+                        field_id = 0
+                        try:
+                            field_id = int(other_field[len(ID_FIELD_BEGINNING):])
+                        except ValueError:
+                            error = protocol.create_error("Parsing", "Non-numerical todo ID",
+                            "The todo in line {} has a nonnumerical ID, but needs "
+                            "something like \"id 1234\"".format(todo_tag.linenumber))
+                            db.session.add(error)
+                            db.session.commit()
+                            return
+                        todo = Todo.query.filter_by(id=field_id).first()
+                if todo is None:
+                    todo = Todo(who=who, description=what, tags="", done=False)
+                    db.session.add(todo)
+                todo.protocols.append(protocol)
+                todo_tags_internal = todo.tags.split(";")
+                for other_field in todo_tag.values[2:]:
+                    if other_field.startswith(ID_FIELD_BEGINNING):
+                        continue
+                    elif other_field == "done":
+                        todo.done = True
+                    elif other_field not in todo_tags_internal:
+                        todo_tags_internal.append(other_field)
+                todo.tags = ";".join(todo_tags_internal)
+                db.session.commit()
+            old_decisions = list(protocol.decisions)
+            for decision in old_decisions
+                protocol.decisions.remove(decision)
+            db.session.commit()
+            decision_tags = [tag for tag in tags if tag.name == "beschluss"]
+            for decision_tag in decision_tags:
+                if len(decision_tag.values) == 0:
+                    error = protocol.create_error("Parsing", "Empty decision found.",
+                        "The decision in line {} is empty.".format(decision_tag.linenumber))
+                    db.session.add(error)
+                    db.session.commit()
+                    return
+                decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0])
+                db.session.add(decision)
+                db.session.commit()
+
+
+            
+
+
+@celery.task
+def compile_async(name, document_id):
+    is_error = False
+    try:
+        current = os.getcwd()
+        os.chdir("latex")
+        os.makedirs("bin", exist_ok=True)
+        command = [
+            "/usr/bin/xelatex",
+            "-halt-on-error",
+            "-file-line-error",
+            "-output-directory", "bin",
+            "{}.tex".format(name)
+        ]
+        subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+        subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+        os.chdir(current)
+        os.makedirs("documents/pdf", exist_ok=True)
+        shutil.copy("latex/bin/{}.pdf".format(name), "documents/pdf/")
+        for typ in ["pdf", "log", "aux", "out"]:
+            silent_remove("latex/bin/{}.{}".format(name, typ))
+        silent_remove("latex/{}.tex".format(name))
+    except subprocess.SubprocessError:
+        is_error = True
+        with app.app_context():
+            document = Document.query.filter_by(id=document_id).first()
+            if document is not None:
+                document.ready = False
+                document.error = True
+                db.session.commit()
+    finally:
+        # TODO: activate deleting files in error case too
+        #for typ in ["pdf", "log", "aux", "out"]:
+        #    silent_remove("latex/bin/{}.{}".format(name, typ))
+        #silent_remove("latex/{}.tex".format(name))
+        os.chdir(current)
+    if not is_error:
+        with app.app_context():
+            document = Document.query.filter_by(id=document_id).first()
+            if document is not None:
+                document.ready = True
+                db.session.commit()
+
+def send_mail(mail):
+    send_mail_async.delay(mail.id)
+
+@celery.task
+def send_mail_async(mail_id):
+    with app.app_context():
+        mail = Mail.query.filter_by(id=mail_id).first()
+        if mail is None:
+            return False
+        mail.ready = False
+        mail.error = False
+        db.session.commit()
+        result = mail_manager.send(mail.to_addr, mail.subject, mail.content)
+        mail.ready = True
+        mail.error = not result
+        db.session.commit()
+
+
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..edf0aa3a6d7dcfd4bb283866974baea0496d1b4d
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,6 @@
+{% extends "layout.html" %}
+{% block title %}Hauptseite{% endblock %}
+
+{% block content %}
+    Dies ist die Startseite
+{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
new file mode 100644
index 0000000000000000000000000000000000000000..ff0ec5a2e0530ea282f1fa2011d727fb6c8f5f29
--- /dev/null
+++ b/templates/layout.html
@@ -0,0 +1,41 @@
+<!doctype html>
+<html>
+<head>
+    {% block head %}
+    <meta charset="utf-8" />
+    <meta name="description" content="Protokollsystem" />
+    <link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}" />
+    {% block additional_js %}
+    {% endblock %}
+    <title>{% block title %}Unbenannte Seite{% endblock %} - Protokollsystem</title>
+    {% endblock %}
+</head>
+<body>
+<div class="header">
+    <p class="header-title"><a href="{{ url_for("index") }}">Protokollsystem</a></p>
+    {% block additional_links %}
+    {% endblock %}
+</div>
+<div class="main">
+{% with messages = get_flashed_messages(with_categories=true) %}
+{% if messages %}
+{% for category, message in messages %}
+    <div class="section alert {{ category }}">
+        {{ message }}
+    </div>
+{% endfor %}
+{% endif %}
+{% endwith %}
+{% block content %}
+Diese Seite ist leer.
+{% endblock %}
+{% block footer %}
+<div class="footer">
+    <p class="footer-text">System zur Erstellung und Verwaltung von Sitzungsprotokollen, entwickelt von Robin Sonnabend</p>
+    <p class="footer-link"><a href="{{ url_for("imprint") }}">Impressum</a></p>
+    <p class="footer-link"><a href="{{ url_for("contact") }}">Kontakt</a></p>
+    <p class="footer-link"><a href="{{ url_for("privacy") }}">Datenschutzerklärung</a></p>
+</div>
+{% endblock %}
+</div>
+</body>
diff --git a/templates/layout_simple.html b/templates/layout_simple.html
new file mode 100644
index 0000000000000000000000000000000000000000..31b0c14ded2937201b3c0990a6f1c9b48bd76fcf
--- /dev/null
+++ b/templates/layout_simple.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html>
+<head>
+    {% block head %}
+    <meta charset="utf-8" />
+    <meta name="description" content="Protokollsystem" />
+    <link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}" />
+    {% block additional_js %}
+    {% endblock %}
+    <title>{% block title %}Unbenannte Seite{% endblock %} - Protokollsystem</title>
+    {% endblock %}
+</head>
+<body>
+{#
+{% with messages = get_flashed_messages(with_categories=true) %}
+{% if messages %}
+{% for category, message in messages %}
+    <div class="section alert {{ category }}">
+        {{ message }}
+    </div>
+{% endfor %}
+{% endif %}
+{% endwith %}
+#}
+{% block content %}
+Diese Seite ist leer.
+{% endblock %}
+</body>
diff --git a/templates/pad_index.html b/templates/pad_index.html
new file mode 100644
index 0000000000000000000000000000000000000000..17c580f13a8a2210c40737da3e9fea8a1f817f3d
--- /dev/null
+++ b/templates/pad_index.html
@@ -0,0 +1,12 @@
+{% extends "layout_simple.html" %}
+{% block title %}Pad - Padname?{% endblock %}
+{% block additional_js %}
+<script src="{{ url_for("static", filename="js/socket_io.js") }}"></script>
+<script src="{{ url_for("static", filename="js/pad.js") }}"></script>
+{% endblock %}
+
+{% block content %}
+    <div id="padarea" class="pad-content">
+        
+    </div>
+{% endblock %}
diff --git a/templates/protokoll.tex b/templates/protokoll.tex
new file mode 100644
index 0000000000000000000000000000000000000000..5616e1b832205d21bda09d1070dfac2bce627e7e
--- /dev/null
+++ b/templates/protokoll.tex
@@ -0,0 +1,52 @@
+\documentclass[11pt,twoside]{protokoll2}
+%\usepackage{bookman}
+%\usepackage{newcent}
+%\usepackage{palatino}
+\usepackage{pdfpages}
+\usepackage{eurosym}
+\usepackage[utf8]{inputenc}
+\usepackage[pdfborder={0 0 0}]{hyperref}
+\usepackage{ngerman}
+% \usepackage[left]{lineno}
+%\usepackage{footnote}
+%\usepackage{times}
+\renewcommand{\thefootnote}{\fnsymbol{footnote}}
+\renewcommand{\thempfootnote}{\fnsymbol{mpfootnote}}
+%\renewcommand{\familydefault}{\sfdefault}
+\newcommand{\einrueck}[1]{\hfill\begin{minipage}{0.95\linewidth}#1\end{minipage}}
+
+\begin{document}
+%\thispagestyle{plain}   %ggf kommentarzeichen entfernen
+\Titel{
+\large Protokoll: \VAR{protocol.protocoltype.name|escape_tex}
+\\\normalsize \VAR{protocol.protocoltype.organization|escape_tex}
+}{}
+\begin{tabular}{rp{15.5cm}}
+{\bf Datum:} & \VAR{protocol.date|datify_long|escape_tex}\\
+{\bf Ort:} & \VAR{protocol.location|escape_tex}\\
+{\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\
+{\bf Anwesend:} & \VAR{protocol.participants|join(", ")|escape_tex}\\
+\end{tabular}
+\normalsize
+
+\section*{Beschlüsse}
+\begin{itemize}
+\ENV{if protocol.decisions|length > 0}
+    \ENV{for decision in protocol.decisions}
+        \item \VAR{decisions.content|escape_tex}
+    \ENV{endfor}
+\ENV{else}
+	\item Keine Beschlüsse
+\ENV{endif}
+\end{itemize}
+
+Beginn der Sitzung: \VAR{protocol.start_time|timify}
+
+\ENV{for top in tree.children}
+    \TOP{\VAR{top.name}} % here we probably have information doubly
+    \VAR{top.render()}
+\ENV{endfor}
+
+Ende der Sitzung: \VAR{protocol.end_time|timify}
+
+\end{document}
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1cd9f733acd060f659a27390f3059b5a70f23ec
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,92 @@
+from flask import render_template
+
+import random
+import string
+import regex
+import smtplib
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from datetime import datetime, date, timedelta
+
+import config
+
+def random_string(length):
+    return "".join((random.choice(string.ascii_letters) for i in range(length)))
+
+def is_past(some_date):
+    return (datetime.now() - some_date).total_seconds() > 0
+
+def encode_kwargs(kwargs):
+    encoded_kwargs = {}
+    for key in kwargs:
+        value = kwargs[key]
+        if hasattr(value, "id"):
+            encoded_kwargs[key] = (type(value), value.id, True)
+        else:
+            encoded_kwargs[key] = (type(value), value, False)
+    return encoded_kwargs
+
+def decode_kwargs(encoded_kwargs):
+    kwargs = {}
+    for name in encoded_kwargs:
+        kind, id, from_db = encoded_kwargs[name]
+        if from_db:
+            kwargs[name] = kind.query.filter_by(id=id).first()
+        else:
+            kwargs[name] = id
+    return kwargs
+        
+
+class UrlManager:
+    def __init__(self, config):
+        self.pattern = regex.compile(r"(?:(?<proto>https?):\/\/)?(?<hostname>[[:alnum:]_.]+(?:\:[[:digit:]]+)?)?(?<path>(?:\/[[:alnum:]_#]*)+)?(?:\?(?<params>.*))?")
+        self.base = "{}://{}{}{}"
+        self.proto = getattr(config, "URL_PROTO", "https")
+        self.root = getattr(config, "URL_ROOT", "example.com")
+        self.path = getattr(config, "URL_PATH", "/")
+        self.params = getattr(config, "URL_PARAMS", "")
+
+    def complete(self, url):
+        match = self.pattern.match(url)
+        if match is None:
+            return None
+        proto = match.group("proto") or self.proto
+        root = match.group("hostname") or self.root
+        path = match.group("path") or self.path
+        params = match.group("params") or self.params
+        return self.base.format(proto, root, path, "?" + params if len(params) > 0 else "")
+
+url_manager = UrlManager(config)
+
+class MailManager:
+    def __init__(self, config):
+        self.active = getattr(config, "MAIL_ACTIVE", False)
+        self.from_addr = getattr(config, "MAIL_FROM", "")
+        self.hostname = getattr(config, "MAIL_HOST", "")
+        self.username = getattr(config, "MAIL_USER", "")
+        self.password = getattr(config, "MAIL_PASSWORD", "")
+        self.prefix = getattr(config, "MAIL_PREFIX", "")
+
+    def send(self, to_addr, subject, content):
+        if (not self.active
+            or not self.hostname
+            or not self.username
+            or not self.password
+            or not self.from_addr):
+            return True
+        try:
+            msg = MIMEMultipart("alternative")
+            msg["From"] = self.from_addr
+            msg["To"] = to_addr
+            msg["Subject"] = "[{}] {}".format(self.prefix, subject) if self.prefix else subject
+            msg.attach(MIMEText(content, _charset="utf-8"))
+            server = smtplib.SMTP_SSL(self.hostname)
+            server.login(self.username, self.password)
+            server.sendmail(self.from_addr, to_addr, msg.as_string())
+            server.quit()
+        except Exception as e:
+            print(e)
+            return False
+        return True
+
+mail_manager = MailManager(config)