Skip to content

Storage Backends

David Benson edited this page Oct 7, 2026 · 1 revision

How draw.io opens, saves and synchronises files in the different storage locations (Google Drive, OneDrive, GitHub, the device, the browser and so on), and what a self-hosted deployment needs to make the cloud integrations work. For developers who self-host, debug load and save problems, or want to add a storage location.

All the classes named here live in src/main/webapp/js/diagramly/. The OAuth servlets are in src/main/server/java/com/mxgraph/online/.

The three layers

Layer Class Responsibility
Application App (App.js), a subclass of EditorUi Holds one client per provider (this.drive, this.oneDrive, this.m365, this.gitHub, this.gitLab, this.dropbox, this.trello), the current storage mode (this.mode) and the current file. Maps the URL hash to a provider and dispatches create, open, pick and save to it.
Client DrawioClient (DrawioClient.js) and one subclass per provider Authentication, the current user (setUser fires userChanged), token persistence (getPersistentToken / setPersistentToken, keyed by cookieName) and the provider's REST API: getFile, insertFile, saveFile, pickFile, pickFolder, getLibrary, logout.
File DrawioFile (DrawioFile.js) and one subclass per provider One open document: its data, title, hash, descriptor (provider metadata such as the etag), modified state, autosave, save, conflict handling and merge.

DrawioFile and DrawioClient both extend mxEventSource. Libraries (custom shape libraries) reuse the file classes: each provider has a *Library subclass of its file class.

Providers at a glance

Provider App.MODE_* Hash prefix Client File / library classes Default
Google Drive MODE_GOOGLE ('google') G DriveClient DriveFile, DriveLibrary on (gapi=0 disables)
OneDrive MODE_ONEDRIVE ('onedrive') W OneDriveClient OneDriveFile, OneDriveLibrary on (od=0 disables)
Microsoft 365 (SharePoint) MODE_M365 ('m365') M OneDriveClient in SharePoint mode OneDriveFile with isSP on except in the desktop app and embed mode (ms365=0 disables)
GitHub MODE_GITHUB ('github') H GitHubClient GitHubFile, GitHubLibrary on (gh=0 disables)
GitLab MODE_GITLAB ('gitlab') A GitLabClient (extends GitHubClient) GitLabFile (extends GitHubFile), GitLabLibrary on (gl=0 disables)
Dropbox MODE_DROPBOX ('dropbox') D DropboxClient DropboxFile, DropboxLibrary off (db=1 enables)
Trello MODE_TRELLO ('trello') T TrelloClient TrelloFile, TrelloLibrary off (tr=1 enables)
Device MODE_DEVICE ('device') none none LocalFile, LocalLibrary on
Browser MODE_BROWSER ('browser') L none StorageFile, StorageLibrary on (browser=0 hides it)
Embed host MODE_EMBED ('embed') E none EmbedFile embed mode only

App.MODE_ATLAS also exists and is used by the Atlassian integrations, which are not part of this repository.

Deprecations announced in this repository's discussions:

  • Dropbox: jgraph/drawio#5740, stops working after 31 December 2027. The client only loads with db=1, DropboxFile.prototype.isEditable returns false (files open read-only, use Save As to move them), and Dropbox is no longer offered as a save target.
  • GitLab: jgraph/drawio#5773, stops working after 31 December 2027. GitLab is no longer offered as a save target in the save dialog (addStorageEntry in SaveDialog, Dialogs.js).

URL hash forms

The part of the URL after # names the open file. App.prototype.loadFile(id) reads the first character. The letters for cloud providers go through App.prototype.getModeForChar and getServiceForName to a client, whose getFile(id, success, error) returns a DrawioFile. Each file class writes its own hash back with getHash().

Prefix Meaning Example
G Google Drive file ID (DriveFile.getHash returns 'G' + id) #G<fileId>
W OneDrive item, <driveId>/<itemId> URI-encoded (OneDriveFile.getIdOf) #W<driveId>%2F<itemId>
M SharePoint item via the Microsoft 365 client, same ID format #M<driveId>%2F<itemId>
H GitHub org/repo/ref/path, URI-encoded as a whole #Hjgraph%2Fdrawio-diagrams%2Fdev%2Fdiagrams%2Fschema.xml
A GitLab group/project/ref/path, URI-encoded as a whole #Amygroup%2Fmyproject%2Fmain%2Fdocs%2Fflow.drawio
D Dropbox path relative to the app folder #Dflow.drawio
T Trello attachment: card ID and attachment ID joined by |$| #T<cardId>%7C%24%7C<attachmentId>
L Browser storage, by file title #LMy%20Diagram.drawio
U Load from a URL: directly for the same origin and the hosts in Editor.prototype.isCorsEnabledForUrl (plus the cors URL parameter), otherwise through PROXY_URL. Opens as a local copy (LocalFile with no mode). #Uhttps%3A%2F%2Fjgraph.github.io%2Fdrawio-diagrams%2Fdiagrams%2Fbulb.xml
R Raw diagram data in the URL: XML, or compressed with Graph.compress #R<compressed data>
E Embed mode: reloads the current file from the host via remoteInvoke('getDraftFileContent') #E
create= Creates a file from a JSON descriptor (see the create URL parameter), handled by executeCreateObject #create=%7B...%7D
P URL parameters as URI-encoded JSON, parsed in bootstrap.js before anything else #P%7B%22lightbox%22%3A%221%22%7D
_CONFIG_ Compressed configuration JSON stored into the editor configuration (App.main) #_CONFIG_<compressed json>
S No longer supported for files: shows a deprecation alert

A second # followed by a URI-encoded JSON object can follow the file ID (for example #G<id>#%7B"pageId"%3A"..."%7D). EditorUi.prototype.getDiagramId strips it and getHashObject parses it. The user-facing reference is Supported location hash properties.

Libraries passed in the clibs URL parameter use the same scheme (App.prototype.loadLibraries): L (StorageLibrary), U (UrlLibrary), R (RemoteLibrary, fetched from the embedding host), S (a desktop file path, DesktopLibrary), and G, H, T, D, W through the signed-in client's getLibrary.

File lifecycle

Open

  1. App.prototype.loadFile(id) → peer.getFile(id, success, error). The client downloads the content and descriptor and constructs a file object, for example new OneDriveFile(ui, data, meta).
  2. EditorUi.prototype.fileLoaded(file) closes the previous file, clears the model and the undo history, calls setCurrentFile(file) and file.open(), then fires fileLoaded on the editor.
  3. DrawioFile.prototype.open calls ui.setFileData(data, file), stores a deep copy of the pages as the shadow pages (setShadowPages), installs the change listeners (installListeners) and, if isSyncSupported(), calls startSync().
  4. EditorUi.prototype.setFileData parses the <mxfile>, wraps each <diagram> in a DiagramPage, selects the page from the hash object or page-id parameter, and calls Editor.prototype.setGraphXml for the current page. The format is described in File format.

Edit and the modified flag

installListeners listens to mxEvent.CHANGE on the model and to page format, background, grid, math and other view settings. Each change calls DrawioFile.prototype.fileChanged, which:

  • sets modified (and its copy shadowModified) with setModified(true);
  • if isAutosave() is true, shows "Saving..." and schedules autosave(autosaveDelay, maxAutosaveDelay, ...);
  • otherwise shows the unsaved-changes status (addUnsavedStatus).

isAutosave() is !inConflictState && ui.editor.autosave. Subclasses narrow it: LocalFile autosaves only with a file handle, RemoteFile never. autosaveDelay defaults to 1500 ms (500 ms for OneDriveFile and StorageFile) and can be changed with the autosaveDelay configuration key. maxAutosaveDelay (30 s) forces a save while the user keeps editing. isAutosaveRevision() asks for a new provider revision at most every maxAutosaveRevisionDelay (5 min).

Save

  1. DrawioFile.prototype.save(revision, success, error, unloading, overwrite) refuses read-only files (isEditable()), and files with an invalid checksum unless overwrite is set. It then calls updateFileData(), which serialises the pages with EditorUi.prototype.getFileData (via createData) into file.data.
  2. The subclass's saveFile(title, revision, success, error, unloading, overwrite) sets savingFile = true, takes the current etag unless overwriting, and calls the client, for example OneDriveClient.prototype.saveFile(file, success, error, etag). Changes made during the save are tracked through shadowModified.
  3. On success the subclass stores the new descriptor and calls fileSaved(savedData, lastDesc, success, error). This clears the conflict flags and makes the saved pages the new shadow pages.
  4. handleFileSuccess / handleFileError update the status bar. afterSave(fn) queues work until the current save has finished.

Conflict detection

Every provider uses optimistic locking. The saved revision is compared with the one the file was opened from:

File class Version token Conflict signal (isConflict)
DriveFile desc.etag, sent as If-Match (DriveClient.createUploadRequest) HTTP 412
OneDriveFile meta eTag, sent as If-Match HTTP 412 or 409
GitHubFile blob sha in the contents API request HTTP 409
GitLabFile as GitHub HTTP 400
LocalFile (browser) lastModified of the File System Access handle compared before writing
LocalFile (desktop) mtimeMs from fs.stat, see Desktop app compared before writing
StorageFile hash of the stored data (getEtag) saveFile reloads the stored copy first and merges it if the hash differs

On a conflict the file sets inConflictState. What happens next depends on DrawioFile.SYNC, which is the sync URL parameter:

  • auto (the default in DrawioFile.js; the public PreConfig.js overrides it, see below): DrawioFileSync.fileConflict fetches the latest version, merges it and retries the save.
  • manual: handleConflictError shows showConflictDialog, with Overwrite (save with overwrite = true) or Merge (synchronizeFile, then save). After a failed autosave, the status bar shows a conflict message; clicking it runs synchronizeFile.
  • none: showCopyDialog, with Make a copy or Overwrite.

Merge

synchronizeFile → updateFile → getLatestVersion (the subclass reloads the file) → mergeFile(latestFile). mergeFile does a three-way merge on pages, using the shadow pages as the common ancestor:

  1. ui.diffPages(shadow, remotePages) computes the remote changes. diffPages and patchPages are in DiffSync.js.
  2. If the file is modified, ui.diffPages(shadow, ui.pages) computes the local changes.
  3. It applies the remote patch to a copy of the shadow and compares getHashValueForPages of the result with the remote pages. A mismatch means the diff is not reproducible: checksumError sets invalidChecksum and saving stops until the file is reloaded or overwritten.
  4. Otherwise it applies the patch to the current document with DrawioFile.prototype.patch. With DrawioFile.LAST_WRITE_WINS, local changes win for values and styles. The remote pages become the new shadow.

Real-time collaboration

As the README says, draw.io built from this repository does not support real-time collaborative editing. The public PreConfig.js sets urlParams['sync'] = 'manual', so startSync never creates a DrawioFileSync. The sync classes (DrawioFileSync, P2PCollab, DrawioFilePolling) are in the source, but they rely on backend services that are not part of this repository. In manual mode, concurrent edits by two users are handled by the conflict detection and merge described above.

Drafts and recovery

When EditorUi.enableDrafts is on (drafts=0 turns it off), DrawioFile.prototype.saveDraft keeps unsaved content as .draft_<id> items in IndexedDB (EditorUi.prototype.openDatabase, database database). The same database holds browser-storage files (object stores files and filesInfo), with a fallback to localStorage (StorageFile.getFileContent).

Enabling and disabling providers

Providers are switched on and off by URL parameters, which a deployment can force in PreConfig.js (urlParams['od'] = '0';). The parameters that matter here:

Parameter Effect
gapi=0, od=0, ms365=0, gh=0, gl=0 Disable a provider that is on by default
db=1, tr=1 Enable Dropbox or Trello. A stored mode for either is ignored unless it is enabled (App.prototype.init).
mode=google|onedrive|github|gitlab|dropbox|trello|device|browser Preselects the storage. Otherwise the last mode stored in localStorage (.mode, see App.getStoredMode) is used.
offline=1, local=1, stealth=1, lockdown=1, demo=1 Turn off all cloud providers (Init.js sets gapi, db, od, gh, gl, tr and ms365 to 0)
browser=0, noDevice=1 Hide the browser or device options
sync=none|manual|auto Conflict handling, see above

The provider-related keys of the editor configuration (DRAWIO_CONFIG) are autosaveDelay, oneDriveInlinePicker and enableCustomGitLabUrl. The full parameter list is Supported URL parameters.

In embed mode (embed=1) the cloud clients are not created unless they are enabled explicitly (gapi=1, od=1, gh=1, gl=1, tr=1, ms365=1). The host application stores the file, see Embedding.

OAuth setup for a self-hosted deployment

The client IDs built into the code are those of the draw.io deployment. A self-hosted instance that wants a cloud integration must register its own OAuth app with the provider and configure it in two places:

  1. Client side: a global set in PreConfig.js (loaded before the app, see Configuration).
  2. Server side: the client ID and secret, read by the auth servlet. SecretFacade.getSecret reads them from files in WEB-INF/.

The auth servlets all extend AbsAuth and implement the authorisation-code flow with a server-side code exchange:

  1. The client calls <redirect URI>?getState=1. The servlet stores a random state token in its cache and sets an auth-state cookie.
  2. The client opens the provider's authorise URL in a popup. The state parameter carries cId (client ID), domain (window.location.host), token and, under a sub-path, path.
  3. The provider redirects to the servlet with code. The servlet checks the state token against the cookie (CSRF protection), posts client_id, client_secret, code and (except for GitHub) redirect_uri to the token endpoint, and returns a page that passes the access token to window.opener (for example onOneDriveCallback).
  4. If the provider returns a refresh token, the servlet keeps it in an auth-token<clientId> cookie scoped to the servlet path. Google and OneDrive only ask for one when the user ticks Remember me. Later token refreshes go through the same servlet. The clients keep the access token in memory only.
Provider Redirect URI to register Client-side global WEB-INF/ files Servlet
Google Drive https://<host>/google DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_GOOGLE_APP_ID (project number, used by the Google Picker) google_client_id, google_client_secret GoogleAuthServlet
OneDrive https://<host>/microsoft DRAWIO_MSGRAPH_CLIENT_ID, optional DRAWIO_MSGRAPH_TENANT_ID msgraph_client_id, msgraph_client_secret, optional msgraph_tenant_id MSGraphAuthServlet
GitHub https://<host>/github2 (callback URL of a GitHub App) DRAWIO_GITHUB_ID; for GitHub Enterprise also DRAWIO_GITHUB_URL, DRAWIO_GITHUB_API_URL github_client_id, github_client_secret, optional github_auth_url GitHubAuthServlet
GitLab https://<host>/gitlab DRAWIO_GITLAB_ID, DRAWIO_GITLAB_URL gitlab_client_id, gitlab_client_secret, optional gitlab_auth_url GitlabAuthServlet
Dropbox (deprecated) https://<host>/dropbox DRAWIO_DROPBOX_ID dropbox_client_id, dropbox_client_secret DropboxAuthServlet

The servlet mappings are in the public web.xml. The WEB-INF/*_client_id and *_client_secret files in the repository are placeholders. Notes per provider:

  • Google Drive: scopes are drive.file, drive.install and userinfo.profile (DriveClient.prototype.scopes). Also add your origin as an authorised JavaScript origin. The docker-drawio README walks through the Google console.
  • OneDrive: see Setting up a OneDrive project.
  • Microsoft 365 (SharePoint): the second OneDriveClient (this.m365) redirects to <DRAWIO_SERVER_URL>ms365, and no servlet in this repository serves that path. Disable it with ms365=0 when you self-host.
  • GitHub: GitHubClient uses the client ID of a GitHub App and sends no redirect_uri, so GitHub uses the callback URL registered with the app. GitHubClient.prototype.showAuthorizeDialog and the repository picker link to draw.io's own GitHub App installation page. Change these links if you register your own app.
  • GitLab: the scopes are api read_repository write_repository. A GitLab URL other than https://gitlab.com (from DRAWIO_GITLAB_URL or the gitlab URL parameter) is refused unless Editor.enableCustomGitLabUrl is true (configuration key enableCustomGitLabUrl).
  • Trello: authenticates in the browser with Trello's client.js and TrelloClient.prototype.key. It has no servlet.

Common to all servlets:

  • Multiple apps: AbsAuth.Config accepts several client IDs and secrets in one file, separated by /:::/, in the same order in both files.
  • Host name: the redirect URI is built as https:// + the domain from the state + the servlet path, so register it exactly as users reach the site.
  • HTTP: DRAWIO_USE_HTTP=1 in the server environment builds http:// redirect URIs. This is insecure; use it only for local testing.
  • Sub-path deployments: Google, OneDrive, GitLab and Dropbox send the redirect path (for example /drawio/microsoft). The servlet only accepts a path that ends with its own path, and the cookies are scoped to the context path plus the servlet path (AbsAuth.getCookiePath). Register the full sub-path URI with the provider.

The docker-drawio image does all of this from environment variables (DRAWIO_GOOGLE_*, DRAWIO_MSGRAPH_*, DRAWIO_GITLAB_*, DRAWIO_SERVER_URL). It writes the globals into PreConfig.js and the IDs and secrets into WEB-INF/, and disables Dropbox, GitHub and Trello. See Self-hosting.

Adding a storage backend

There is no plugin API for storage providers. Adding one means changing the sources (see Building). Follow an existing provider. GitLabClient / GitLabFile, which subclass the GitHub classes, show how little a variant needs to override.

  1. Client: MyClient = function(ui) { DrawioClient.call(this, ui, 'myAuthInfo'); } with mxUtils.extend(MyClient, DrawioClient). Implement:

    • getFile(id, success, error), which passes a file object to success;
    • insertFile(title, data, success, error, asLibrary, folderId) and saveFile(file, success, error, ...);
    • pickFile(fn), pickFolder(fn), pickLibrary(fn), getLibrary(id, success, error) and logout();
    • the extension property.

    Call setUser(new DrawioUser(id, email, name)) after signing in so that userChanged fires.

  2. File: MyFile = function(ui, data, meta) { DrawioFile.call(this, ui, data); this.meta = meta; } with mxUtils.extend(MyFile, DrawioFile). Override:

    • getHash() (a new prefix letter plus the URI-encoded ID), getId(), getMode() and getTitle();
    • saveFile(title, revision, success, error, unloading, overwrite): maintain savingFile, send the current etag, call fileSaved on success, and set inConflictState on a conflict;
    • isConflict(err) and getLatestVersion(success, error), needed for merging;
    • getDescriptor / setDescriptor and getDescriptorEtag / setDescriptorEtag;
    • optionally isRenamable / rename, isAutosaveOptional and getFileUrl.
  3. Library: MyLibrary extends MyFile, as OneDriveLibrary does.

  4. App wiring in App.js:

    • add an App.MODE_* constant;
    • create the client in App.prototype.init and listen to userChanged there;
    • map the prefix in getModeForChar, and the mode in getServiceForName, getTitleForService and isModeEnabled;
    • dispatch creation in createFile, and handle the mode in pickFolder, pickLibrary, saveLibrary and loadLibraries;
    • show the user in toggleUserPanel and updateHeader.
  5. UI:

    • storage choices in StorageDialog, SplashDialog, NewDialog and SaveDialog (Dialogs.js);
    • the Open from, Import from and library menus in Menus.prototype.init (Menus.js);
    • the logo and name in AuthDialog.getServiceInfo.
  6. Server: if the provider needs a client secret, add an AbsAuth subclass. Override getConfig() (secret file names, REDIRECT_PATH, AUTH_SERVICE_URL) and processAuthResponse (calls your window.opener callback). Add the servlet and a mapping to web.xml.

  7. Build: add the new source files to the build (Building) and to Devel.js for dev=1. Add the provider's API hosts to your Content-Security-Policy.

See also

Clone this wiki locally