> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/webviewer-annotations-sqlite3.md).

# Save and Load Annotations with SQLite3 and XFDF, JavaScript

Save and load annotations to a server-side database using SQLite3 and XFDF.  Sample code for WebViewer provided in JavaScript

In addition to showing how to add a WebViewer iFrame component to a Vanilla JS client app, this sample expands on annotation functionality by enabling the saving and loading of annotation objects into an SQLite3 database on the server side.

The saving and loading are done through GET and POST HTTP messages that are handled by an `AnnotationController` in the project. The annotations are stored as [XFDF](/web/annotation/xfdf.md) strings embedded in the messages and saved in the server-side database.

WebViewer provides a slick out-of-the-box responsive UI that enables you to view, annotate and manipulate PDFs and other document types inside any web project.

Click the button below to view the full project in GitHub.

{% tabs %}
{% tab title="index.js" %}
{% code title="index.js" lineNumbers="true" %}

```js

var viewerElement = document.getElementById('viewer');
var DOCUMENT_ID = 'webviewer-demo-1';

WebViewer.Iframe({
  path: 'lib',
  initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/demo.pdf',
  documentXFDFRetriever: async () => {
    const rows = await loadxfdfStrings(DOCUMENT_ID);
    return JSON.parse(rows).map(row => row.xfdfString);
  }
}, viewerElement).then(instance => {
  var docViewer = instance.Core.documentViewer;
  var annotManager = docViewer.getAnnotationManager();

  // Save when annotation change event is triggered (adding, modifying or deleting of annotations)
  annotManager.addEventListener('annotationChanged', function(annots, action, options) {
    // If the event is triggered by importing then it can be ignored
    // This will happen when importing the initial annotations from the server or individual changes from other users
    if (options.imported) return;

    annotManager.exportAnnotationCommand().then(function (xfdfStrings) {
      annots.forEach(function(annot) {
        savexfdfString(DOCUMENT_ID, annot.Id, xfdfStrings);
      });
    });
  });
});

// Make a POST request with document ID, annotation ID and XFDF string
var savexfdfString = function(documentId, annotationId, xfdfString) {
  return new Promise(function(resolve) {
    fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
      method: 'POST',
      body: JSON.stringify({
        annotationId,
        xfdfString
      })
    }).then(function(res) {
      if (res.status === 200) {
        resolve();
      }
    });
  });
};

// Make a GET request to get XFDF string
var loadxfdfStrings = function(documentId) {
  return new Promise(function(resolve) {
    fetch(`/server/annotationHandler.js?documentId=${documentId}`, {
      method: 'GET'
    }).then(function(res) {
      if (res.status === 200) {
        res.text().then(function(xfdfStrings) {
          resolve(xfdfStrings);
        });
      }
    });
  });
};

```

{% endcode %}
{% endtab %}

{% tab title="annotationHandler.js" %}
{% code title="annotationHandler.js" lineNumbers="true" %}

```js
// WARNING: In this sample, the query inputs are not sanitized. For production use, you should use sql builder
// libraries like Knex.js (https://knexjs.org/) to prevent SQL injection.

const fs = require('fs');
const SQLite3 = require('sqlite3').verbose();
const TABLE = 'annotations';

module.exports = (app) => {
  // Create a database if it doesn't exist
  if (!fs.existsSync('server/xfdf.db')) {
    fs.writeFileSync('server/xfdf.db', '');
  }

  // Create annotations table with columns documentId, annotationID and xfdfString
  const db = new SQLite3.Database('server/xfdf.db');
  db.run(`CREATE TABLE IF NOT EXISTS ${TABLE} (documentId TEXT, annotationId TEXT PRIMARY KEY, xfdfString TEXT)`);
  db.close();

  // Handle POST request sent to '/server/annotationHandler.js'
  app.post('/server/annotationHandler.js', (req, res) => {
    const documentId = req.query.documentId;
    const annotationId = JSON.parse(req.body).annotationId;
    const xfdfString = JSON.parse(req.body).xfdfString.replace(/\'/g, `''`); // To escape single quote character in SQLite

    const db = new SQLite3.Database('server/xfdf.db');
    db.serialize(() => {
      const isDeleteCommand = /<delete>(.*)<\/delete>/s.test(xfdfString);

      let query;
      if (isDeleteCommand) {
        // Instead of saving the delete command, we can remove the row from the database
        query = `DELETE FROM ${TABLE} WHERE annotationId = '${annotationId}'`;
      } else {
        // Save document ID, annotation ID and XFDF string to database
        query = `INSERT OR REPLACE INTO ${TABLE} VALUES ('${documentId}', '${annotationId}', '${xfdfString}')`;
      }

      db.run(query, err => {
        if (err) {
          res.status(500);
        } else {
          res.status(200);
        }
        res.end();
      });
    });
    db.close();
  });

  // Handle GET request sent to '/server/annotationHandler.js'
  app.get('/server/annotationHandler.js', (req, res) => {
    const documentId = req.query.documentId;

    const db = new SQLite3.Database('server/xfdf.db');
    // Read from the database and send the rows as a response
    db.all(`SELECT annotationId, xfdfString FROM ${TABLE} WHERE documentId = '${documentId}'`, (err, rows) => {
      if (err) {
        res.status(204);
      } else {
        res.header('Content-Type', 'application/json');
        res.status(200).send(rows);
      }
      res.end();
    });
    db.close();
  });
}

```

{% endcode %}
{% endtab %}
{% endtabs %}

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/webviewer-annotations-sqlite3)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/webviewer-annotations-sqlite3.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
