diff --git a/extension/Classes/Core/Form/FormAsFile.php b/extension/Classes/Core/Form/FormAsFile.php
index d8aba61ab8ebbecd4567eb968190cba06d156bdd..f3015a02b6f27b62971b286061bfc397680ac7f1 100644
--- a/extension/Classes/Core/Form/FormAsFile.php
+++ b/extension/Classes/Core/Form/FormAsFile.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 namespace IMATHUZH\Qfq\Core\Form;
 
 use IMATHUZH\Qfq\Core\Database\Database;
+use IMATHUZH\Qfq\Core\Exception\Thrower;
 use IMATHUZH\Qfq\Core\Helper\HelperFile;
 use IMATHUZH\Qfq\Core\Helper\OnString;
 use IMATHUZH\Qfq\Core\Helper\Path;
@@ -16,10 +17,10 @@ class FormAsFile
      * Remove the form from the DB and insert it using the form file. (only if the form file was changed)
      * If the form file can't be read, then the form is deleted from the DB and an exception is thrown.
      *
-     * Form file location: SYSTEM_FORM_FILE_PATH
-     * Recognize form file change: Compare the current file stats with the ones saved in the Form table.
-     * Container References: The form file uses names instead if ids to reference container formElements. These references are translated when the formElements are inserted.
-     * Ignored keys: The keys 'id', 'formId', 'feIdContainer' are ignored when reading the form file.
+     * - Form file location: SYSTEM_FORM_FILE_PATH
+     * - Recognize form file change: Compare the current file stats with the ones saved in the Form table.
+     * - Container References: The form file uses names instead if ids to reference container formElements. These references are translated when the formElements are inserted.
+     * - Ignored keys: The keys 'id', 'formId', 'feIdContainer' are ignored when reading the form file.
      *
      * @param string $formName
      * @param Database $database
@@ -108,67 +109,31 @@ class FormAsFile
     }
 
     /**
-     * Reads the form from the DB and saves it in the form folder as a form file.
-     * Warning: Overwrites form file without any checks or warning.
+     * Reads the form from the database and saves it in the form folder as a form file.
+     * If $formId is given, then $formName is ignored.
      * If the form file path does not exist, it is created and all forms are exported.
      *
-     * FileStats: After the export the column "fileStats" is updated with new stats from new file.
-     * Container FormElements: The form file uses names instead if ids to reference containers. These references are translated before saving the file.
-     * Ignored columns: The columns 'id', 'name' (only Form table), 'fileStats', 'formId', 'feIdContainer' are not saved in the file.
-     * FormElement order: The formElements are ordered by the 'ord' column before writing them to the file.
+     * ! Warning: Overwrites form file without any checks or warning.
+     *
+     * - FileStats: After the export the column "fileStats" is updated with new stats from new file.
+     * - Container FormElements: The form file uses names instead if ids to reference containers. These references are translated before saving the file.
+     * - Ignored columns: The columns 'id', 'name' (only Form table), 'fileStats', 'formId', 'feIdContainer' are not saved in the file.
+     * - FormElement order: The formElements are ordered by the 'ord' column before writing them to the file.
      *
      * @param string $formName
      * @param Database $database
+     * @param int|null $formId if given, then $formName is ignored
      * @throws \CodeException
      * @throws \DbException
      * @throws \UserFormException
      */
-    public static function exportForm(string $formName, Database $database) // : void
+    public static function exportForm(string $formName, Database $database, ?int $formId = null) // : void
     {
-        // Get form from DB
-        list($sql, $parameterArray) = SqlQuery::selectFormByName($formName);
-        $form = $database->sql($sql, ROW_EXPECT_1,
-            $parameterArray, 'Form "' . $formName . '" not found or multiple forms with the same name.'); // array(column name => value)
-
-        // Remove columns: id, name, fileStats
-        $formId = $form[F_ID];
-        unset($form[F_ID]);
-        unset($form[F_NAME]);
-        unset($form[F_FILE_STATS]);
-
-        // Get formElements from DB
-        list($sql, $parameterArray) = SqlQuery::selectFormElementById($formId);
-        $formElements = $database->sql($sql, ROW_REGULAR, $parameterArray); // array(array(column name => value))
-
-        // Translate container references (id to name) and remove all id columns
-        $containerNames = array_reduce($formElements, function ($result, $formElement) {
-            if ($formElement[FE_CLASS] === FE_CLASS_CONTAINER) {
-                $containerName = $formElement[FE_NAME];
-                if (in_array($containerName, $result) || $containerName === '') {
-                    throw new \UserFormException(json_encode([
-                        ERROR_MESSAGE_TO_USER => 'Duplicate container names.',
-                        ERROR_MESSAGE_TO_DEVELOPER => "Container Form Elements must have a unique and nonempty name. Container name: '$containerName'."]),
-                        ERROR_FORM_INVALID_NAME);
-                }
-                $result[$formElement[FE_ID]] = $containerName;
-            }
-            return $result;
-        }, []); // array(id => name)
-        $formElements = array_map(function ($formElement) use ($containerNames) {
-            $containerId = $formElement[FE_ID_CONTAINER];
-            if ($containerId !== 0) {
-                $formElement[FE_FILE_CONTAINER_NAME] = $containerNames[$containerId];
-            }
-            unset($formElement[FE_ID_CONTAINER]);
-            unset($formElement[FE_ID]);
-            unset($formElement[FE_FORM_ID]);
-            return $formElement;
-        }, $formElements);
+        list($formName, $formId, $formJson) = self::formToJson($formName, $database, $formId);
 
-        // write form as JSON to file
-        $form[F_FILE_FORM_ELEMENT] = $formElements;
-        $formJson = json_encode($form, JSON_PRETTY_PRINT);
+        // backup and write file
         $pathFileName = self::formPathFileName($formName, $database);
+        self::backupFormFile($pathFileName);
         HelperFile::file_put_contents($pathFileName, $formJson);
 
         // Update column fileStats
@@ -177,6 +142,30 @@ class FormAsFile
         $database->sql($sql, ROW_REGULAR, $parameterArray);
     }
 
+    /**
+     * Create copy of given form file in form/backup. If given file does not exist, do nothing.
+     * New file name: <formName>.YYYMMDDhhmmss.file.json
+     *
+     * @param string $pathFileName
+     * @throws \UserFormException
+     */
+    private static function backupFormFile(string $pathFileName)
+    {
+        if (file_exists($pathFileName))
+        {
+            if (!is_readable($pathFileName)) {
+                Thrower::userFormException('Error while trying to backup form file.', "Form file is not readable: $pathFileName");
+            }
+
+            // copy file
+            $cwdToBackupFile = self::newBackupPathFileName(basename($pathFileName, '.json'), 'file');
+            $success = copy($pathFileName, $cwdToBackupFile);
+            if ($success === false) {
+                Thrower::userFormException('Error while trying to backup form file.', "Can't copy file $pathFileName to $cwdToBackupFile");
+            }
+        }
+    }
+
     /**
      * Import Form from file if loaded record is Form/FormElement.
      * If form file was changed, import it and throw exception.
@@ -219,6 +208,7 @@ class FormAsFile
         self::enforceFormFileWritable($formName, $database);
         $pathFileName = self::formPathFileName($formName, $database);
         if(file_exists($pathFileName)) {
+            self::backupFormFile($pathFileName);
             $success = unlink($pathFileName);
             if ($success === false) {
                 throw new \UserFormException(json_encode([
@@ -310,7 +300,7 @@ class FormAsFile
      */
     public static function isFormQuery(string $sql): bool
     {
-        // find substrings whihch start with FROM and are followed by Form or FormElement
+        // find substrings which start with FROM and are followed by Form or FormElement
         preg_match_all('/(?i)FROM(?-i)(.*?)\b(' . TABLE_NAME_FORM . '|' . TABLE_NAME_FORM_ELEMENT . ')\b/s', $sql, $matches);
 
         // Check if no other SQL keywords are in between FROM and the table name
@@ -441,12 +431,32 @@ class FormAsFile
      */
     private static function deleteFormDBWithId(int $formId, Database $database) // : void
     {
+        self::backupFormDb($formId, $database);
         $F_ID = F_ID; // can't use constants in strings directly
         $TABLE_NAME_FORM = TABLE_NAME_FORM; // can't use constants in strings directly
         $database->sql("DELETE FROM `$TABLE_NAME_FORM` WHERE `$F_ID`=? LIMIT 1", ROW_REGULAR, [$formId]);
         self::deleteFormElementsDBWithFormId($formId, $database);
     }
 
+    /**
+     * Export the given form from database into a backup file.
+     * Backup file name: <formName>.YYYMMDDhhmmss.db.json
+     *
+     * @param int $formId
+     * @param Database $database
+     * @throws \CodeException
+     * @throws \DbException
+     * @throws \UserFormException
+     */
+    private static function backupFormDb(int $formId, Database $database)
+    {
+        list($formName, $formId, $formJson) = self::formToJson('', $database, $formId);
+        $cwdToBackupFile = self::newBackupPathFileName($formName, 'db');
+        HelperFile::file_put_contents($cwdToBackupFile, $formJson);
+    }
+
+
+
     /**
      * Delete form elements with given formId from DB
      *
@@ -472,6 +482,7 @@ class FormAsFile
      */
     private static function formFileStatsJson(string $pathFileName)
     {
+        clearstatcache (true, $pathFileName);
         $stats = stat($pathFileName);
         if ($stats === false) {
             return false;
@@ -579,4 +590,102 @@ class FormAsFile
             return $result;
         }, []);
     }
+
+    /**
+     * Return json string of the given form with form-elements. If $formId is given, $formName is ignored.
+     *
+     * - Container FormElements: The form file uses names instead if ids to reference containers. These references are translated before saving the file.
+     * - Ignored columns: The columns 'id', 'name' (only Form table), 'fileStats', 'formId', 'feIdContainer' are not added to the json output.
+     * - FormElement order: The formElements are ordered by the 'ord' column before writing them to the file.
+     *
+     * @param string $formName
+     * @param Database $database
+     * @param int|null $formId If given, $formName is ignored
+     * @return array
+     * @throws \CodeException
+     * @throws \DbException
+     * @throws \UserFormException
+     */
+    private static function formToJson(string $formName, Database $database, ?int $formId = null): array
+    {
+        // Get form from DB
+        if ($formId !== null) {
+            list($sql, $parameterArray) = SqlQuery::selectFormById($formId);
+            $form = $database->sql($sql, ROW_EXPECT_1,
+                $parameterArray, "Form with id $formId not found."); // array(column name => value)
+            $formName = $form[F_NAME];
+        } else {
+            list($sql, $parameterArray) = SqlQuery::selectFormByName($formName);
+            $form = $database->sql($sql, ROW_EXPECT_1,
+                $parameterArray, "Form $formName not found or multiple forms with the same name."); // array(column name => value)
+        }
+
+        // Remove columns: id, name, fileStats
+        $formId = $form[F_ID];
+        unset($form[F_ID]);
+        unset($form[F_NAME]);
+        unset($form[F_FILE_STATS]);
+
+        // Get formElements from DB
+        list($sql, $parameterArray) = SqlQuery::selectFormElementById($formId);
+        $formElements = $database->sql($sql, ROW_REGULAR, $parameterArray); // array(array(column name => value))
+
+        // Translate container references (id to name) and remove all id columns
+        $containerNames = array_reduce($formElements, function ($result, $formElement) {
+            if ($formElement[FE_CLASS] === FE_CLASS_CONTAINER) {
+                $containerName = $formElement[FE_NAME];
+                if (in_array($containerName, $result) || $containerName === '') {
+                    throw new \UserFormException(json_encode([
+                        ERROR_MESSAGE_TO_USER => 'Duplicate container names.',
+                        ERROR_MESSAGE_TO_DEVELOPER => "Container Form Elements must have a unique and nonempty name. Container name: '$containerName'."]),
+                        ERROR_FORM_INVALID_NAME);
+                }
+                $result[$formElement[FE_ID]] = $containerName;
+            }
+            return $result;
+        }, []); // array(id => name)
+        $formElements = array_map(function ($formElement) use ($containerNames) {
+            $containerId = $formElement[FE_ID_CONTAINER];
+            if ($containerId !== 0) {
+                $formElement[FE_FILE_CONTAINER_NAME] = $containerNames[$containerId];
+            }
+            unset($formElement[FE_ID_CONTAINER]);
+            unset($formElement[FE_ID]);
+            unset($formElement[FE_FORM_ID]);
+            return $formElement;
+        }, $formElements);
+
+        // add form elements and create json
+        $form[F_FILE_FORM_ELEMENT] = $formElements;
+        $formJson = json_encode($form, JSON_PRETTY_PRINT);
+        return array($formName, $formId, $formJson);
+    }
+
+    /**
+     * Return the path to a (non-existing) form backup file with name:
+     * <formName>.YYYMMDDhhmmss.<tag>.json
+     *
+     * @param string $formName
+     * @param string $tag
+     * @return string
+     * @throws \UserFormException
+     */
+    private static function newBackupPathFileName(string $formName, string $tag): string
+    {
+        // create backup path if not exists
+        $cwdToBackup = Path::cwdToProject(Path::PROJECT_TO_FORM, Path::FORM_TO_FORM_BACKUP);
+        if (!is_dir($cwdToBackup)) {
+            $success = mkdir($cwdToBackup, 0777, true);
+            if ($success === false) {
+                Thrower::userFormException('Error while trying to backup form file.', "Can't create backup path: $cwdToBackup");
+            }
+        }
+
+        // throw exception if backup file exists
+        $cwdToBackupFile = Path::join($cwdToBackup, $formName . '.' . date('YmdHis') . ".$tag.json");
+        if (file_exists($cwdToBackupFile)) {
+            Thrower::userFormException('Error while trying to backup form file.', "Backup file already exists: $cwdToBackupFile");
+        }
+        return $cwdToBackupFile;
+    }
 }
\ No newline at end of file
diff --git a/extension/Classes/Core/Helper/HelperFile.php b/extension/Classes/Core/Helper/HelperFile.php
index 8622218ae79697bbac7c78ea7df1cf4fe25630f3..a64919e94974a1c2b35b9f09f850b5c99dd9d905 100644
--- a/extension/Classes/Core/Helper/HelperFile.php
+++ b/extension/Classes/Core/Helper/HelperFile.php
@@ -550,6 +550,7 @@ class HelperFile {
 
     /**
      * Wrapper for file_put_contents which throws exception on failure.
+     * Clear the stat cache to make sure that stat(), file_exists(),... etc. return current data.
      *
      * @param $pathFileName
      * @param $content
@@ -564,6 +565,7 @@ class HelperFile {
                 ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$pathFileName'"]),
                 ERROR_IO_WRITE_FILE);
         }
+        clearstatcache (true, $pathFileName);
     }
 
     /**
@@ -636,6 +638,7 @@ class HelperFile {
 
     /**
      * Wrapper for is_readable() but throws exception if file/directory exists but is not readable.
+     * Returns false if file does not exist.
      *
      * @param $pathFileName
      * @return bool
diff --git a/extension/Classes/Core/Helper/Path.php b/extension/Classes/Core/Helper/Path.php
index 19fa8cab3fc2331e87f2474ac2d9df4b62943e3f..979923030953dee373a504e6bd0a66c1980b46b1 100644
--- a/extension/Classes/Core/Helper/Path.php
+++ b/extension/Classes/Core/Helper/Path.php
@@ -33,14 +33,14 @@ class Path
 
     // API
     const EXT_TO_API = 'Classes/Api';
-    const API_TO_APP = '../../../../../'; // TODO: not relative to ext
+    const API_TO_APP = '../../../../../'; // TODO: make relatvie to ext instead
 
     // Icons
     const EXT_TO_GFX_INFO_FILE = 'Resources/Public/icons/note.gif';
     const EXT_TO_PATH_ICONS = 'Resources/Public/icons';
 
     // Annotate
-    const EXT_TO_HIGHLIGHT_JSON_DIR = 'Resources/Public/Json';
+    const EXT_TO_HIGHLIGHT_JSON_DIR = 'Resources/Public/Json';  // TODO: refactor: remove DIR at the end
 
     // Twig
     const EXT_TO_TWIG_TEMPLATES = 'Resources/Public/twig_templates';
@@ -50,13 +50,14 @@ class Path
     private const APP_TO_PROJECT_DEFAULT = '../';
     private const APP_TO_PROJECT_MIGRATE = 'fileadmin/protected/qfqProject';
     const PROJECT_TO_FORM = 'form';
+    const FORM_TO_FORM_BACKUP = '_backup';
     const PROJECT_DIR_TO_REPORT = 'report';
 
     // Config
     const APP_TO_TYPO3_CONF = 'typo3conf';
 
     // Log files
-    private static $cwdToLog  = null; // TODO: does it make sense to have it rel to CWD? to broad?
+    private static $cwdToLog  = null;
     private static $overloadAbsoluteQfqLogFile = null;
     private static $overloadAbsoluteMailLogFile = null;
     private static $overloadAbsoluteSqlLogFile = null;
diff --git a/extension/Classes/Core/Report/ReportAsFile.php b/extension/Classes/Core/Report/ReportAsFile.php
index d274db6f45726175e78fc32081dc159e9956310b..130121d0f9a2f9641dace3b9b4e1a957444fd8d8 100644
--- a/extension/Classes/Core/Report/ReportAsFile.php
+++ b/extension/Classes/Core/Report/ReportAsFile.php
@@ -16,9 +16,7 @@ class ReportAsFile
      *
      * @param string $bodyText
      * @return string|null pathFileName of report relative to CWD or null
-     * @throws \CodeException
      * @throws \UserFormException
-     * @throws \UserReportException
      */
     public static function parseFileKeyword(string $bodyText) // : ?string
     {
@@ -52,7 +50,7 @@ class ReportAsFile
      * Create new file named after the tt_content header or the UID if not defined.
      * - Invalid characters are stripped from filename.
      * - If file exists, throw exception.
-     * - The path of the file is derived the typo3 page structure (use page alias if exists, else page name).
+     * - The path of the file is derived from the typo3 page structure (use page alias if exists, else page name).
      * - The path is created if not exists.
      *
      * @param int $uid
@@ -84,7 +82,7 @@ class ReportAsFile
             if (strlen($reportPath) > 4096) {
                 // Throw exception in case of infinite loop.
                 throw new \UserReportException(json_encode([
-                    ERROR_MESSAGE_TO_USER => "Path too long.",
+                    ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
                     ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
                     ERROR_FORM_NOT_FOUND);
             }
@@ -104,8 +102,8 @@ class ReportAsFile
         // if file exists, throw exception
         if (file_exists($reportPathFileNameFull)) {
             throw new \UserReportException(json_encode([
-                ERROR_MESSAGE_TO_USER => "Writing file failed.",
-                ERROR_MESSAGE_TO_DEVELOPER => "Can't export report to file. File already exists: '$reportPathFileNameFull'"]),
+                ERROR_MESSAGE_TO_USER => "Exporting report to file failed.",
+                ERROR_MESSAGE_TO_DEVELOPER => "File already exists: '$reportPathFileNameFull'"]),
                 ERROR_FORM_NOT_FOUND);
         }