Drive Sync Architecture: URL Layout, Scopes, and Audit Log

This guide explains Developer reference for the Drive Sync WebDAV facade — URL layout, scope enforcement, device-password lifecycle, chunked uploads, and the audit trail. so you can complete the TrekMail task with confidence.

Article details

Type, difficulty, plans, and last updated info.

Type
Reference
Difficulty
Intermediate
Plans
Nano · Starter · Pro · Agency
Last updated
May 22, 2026

This is the developer-facing reference for Drive Sync. If you're integrating sync access alongside the REST API or MCP server, building tooling on top of TrekMail Drive, or auditing how the WebDAV facade enforces permissions, this is the page.

If you're just trying to connect rclone or Finder to your Drive, you're in the wrong doc — start with Drive Sync overview.

What Drive Sync is, technically

Drive Sync is a WebDAV facade in front of the same database tables, B2 object storage, and audit log that the dashboard and webmail use. There's no separate sync engine, no separate file store, no separate auth provider. Every PUT, PROPFIND, MOVE, or DELETE you make over WebDAV ends up calling the same services the dashboard does.

The facade gives you a standards-compliant HTTP surface (defined by RFC 4918 plus a few common extensions) at a fixed prefix, so any WebDAV client speaks to it without TrekMail-specific code.

URL layout

The root of the sync surface is /dav/files/, served from the same domain as your dashboard:

https://YOUR-DOMAIN/dav/files/

Below that root, every device password sees one of two trees depending on its scope:

Account-wide tree

/dav/files/account/
    ├── (top-level account-drive folders the dashboard shows)
    └── (top-level files at the account-drive root)

This is the dashboard's /drive view, served as a WebDAV directory. Account-scoped device passwords (the default) land here.

Mailbox-scoped tree

/dav/files/mailbox-{N}/
    ├── (the mailbox's personal Drive files and folders)
    └── Shared/
        ├── (account-drive folders flagged "shared with all mailboxes")
        └── ...

When a device password is restricted to one mailbox via the dashboard UI, it lands here instead. The Shared/ virtual collection inside the mailbox tree is what the webmail Drive view shows the mailbox user: their personal files plus any folders the account owner shared with the whole account.

A device password can only see one of these trees, never both. Choosing "Limit to a specific mailbox" on the password creation form swaps the visible URL.

Two issuance surfaces (admin vs mailbox)

Device-password rows can be created from two different controllers, both writing to the same drive_app_passwords table:

  • DashboardApp\Http\Controllers\App\DriveAppPasswordController at /drive/devices. Authenticated against the standard web guard; account owners and admins only. Can issue both account-scoped (mailbox_id NULL) and mailbox-scoped rows, and any of the eight drive:* scopes the account is entitled to. Listing shows the full inventory for the account.
  • WebmailApp\Http\Controllers\Webmail\SyncDeviceController at /webmail/api/sync-devices. Authenticated against the mailbox guard (the webmail session has its own cookie and middleware stack, isolated from the dashboard). Always forces mailbox_id to the signed-in mailbox, ignores any user-supplied mailbox_id in the payload, and only offers drive:mailbox:* scopes. Listing is filtered to the signed-in mailbox.

Each row records which surface produced it in a created_via column ('dashboard' | 'webmail'). The dashboard listing renders a small badge per row showing the surface and, for webmail-issued rows, the mailbox address — making cross-surface audit a one-glance read. Existing rows pre-dating the column default to 'dashboard' via the column DEFAULT, which is accurate since the webmail flow didn't exist before.

A per-scope uniqueness check on label blocks creating two active devices with the same name. The check ignores revoked rows so labels free up after revocation. Per-mailbox cap is 50 rows (including revoked) to keep churn-generate scripts from bypassing it; the dashboard enforces a separate 50-row per-account cap.

Both surfaces hit the same auth, the same WebDAV facade, the same audit trail — the split is purely about who's allowed to mint passwords and which subset they can mint.

Auth: device passwords

Sync auth is HTTP Basic over HTTPS, where the username is the account email and the password is a string starting with dsync_. The dsync_ prefix is how the auth middleware tells a sync password apart from an OAuth bearer token or an API token (which uses a different prefix).

Each device password has:

  • A 64-character random secret, generated once at creation, never stored in plain text. The server stores a SHA-256 hash and matches incoming requests against it.
  • A scope set (see below).
  • An optional mailbox constraint (mailbox_id not null → scoped to that mailbox only).
  • An optional expiry timestamp.
  • A revoked flag — set the moment the user clicks Revoke; from that point every request returns 401.
  • An audit trail: every successful auth records the request IP and User-Agent, the last-used timestamp, and links the resulting mutation to this password row in the drive_activity_events table.

If the password is revoked, expired, or hasn't been used in 90 days, the auth middleware returns 401 with a WebDAV-shaped WWW-Authenticate header pointing back at the sign-in. Sync clients ask the user to re-enter credentials, which is the right UX prompt because the password is in fact dead.

Scope model

Drive Sync uses the same scope strings as the REST API, in the drive:{family}:{action} form. The eight that apply to device passwords:

Scope Action
drive:account:read List and download files in the account-drive tree
drive:account:write Upload, rename, move, and trash files in the account-drive tree
drive:account:share Generate public download links for account-drive files
drive:account:purge Permanently delete account-drive files (bypass trash)
drive:mailbox:read Same as account:read but in the mailbox-scoped tree
drive:mailbox:write Same as account:write but mailbox-scoped
drive:mailbox:share Same as account:share but mailbox-scoped
drive:mailbox:purge Same as account:purge but mailbox-scoped

The middleware that gates the WebDAV routes (EnsureDriveDavScope) maps HTTP method + URL prefix to a required scope:

  • Read methods (GET, HEAD, OPTIONS, PROPFIND, REPORT) → :read.
  • Write methods (PUT, POST, PROPPATCH, MKCOL, MOVE, COPY, DELETE, LOCK, UNLOCK) → :write.

The family (account vs mailbox) comes from the URL prefix, not from the password's scope set. That way a device password scoped to one mailbox can never reach the account-wide tree even if both drive:account:* and drive:mailbox:* scopes are on it — the URL prefix /dav/files/account/ requires drive:account:*, and the middleware returns 403 with error="insufficient_scope" if the password doesn't have it.

drive:addon:read (which lets a token read Drive Add-on billing state) is intentionally not exposed in the device-password UI: sync clients have no reason to read billing, and exposing it would be a privilege creep.

Why some clients see "Destructive"

The :purge scopes bypass trash. A normal Edit-permission delete moves a file to the 30-day trash, where it can be restored. A :purge delete is final.

Sync clients almost never need this — rclone, Finder, and friends all use the regular DELETE method, which the middleware maps to :write. The :purge scopes only matter if you're building tooling that explicitly calls into the storage-management endpoints. The dashboard form marks them with a red ring and a "Destructive" tag so users don't grant them by accident.

Filename safety

Every name written through the sync facade — folders, files, both via direct PUT and via the chunked-upload endpoint — passes through DriveNameSanitiser::clean() before persistence. The sanitiser:

  • Rejects empty strings, paths containing / or \, names with null bytes or CRLF control characters.
  • Rejects right-to-left override marks (the U+202E spoof that disguises .exe as .txt).
  • Rejects names ending in . or whitespace (Windows silently strips these and creates collisions).
  • Normalises to Unicode NFC, so a name typed café on a Mac and café on Linux land as the same row.
  • Caps length at 255 visible characters.

If a client sends an unsafe name, the WebDAV response is a 400 with a short message and the request is rejected before touching the database.

Chunked uploads

Files larger than 1 MB go through a separate chunked-upload pipeline at /dav/uploads/{session-uuid}/. This is the Nextcloud chunked-upload protocol (v2): the client opens a session with MKCOL, PUTs numbered chunks into it, then issues a MOVE that assembles them into the final destination.

The TrekMail server holds the in-flight session in a drive_chunked_upload_sessions row with a 24-hour TTL. If a session is abandoned (client crash, network drop), a janitor job sweeps it after the TTL and aborts the underlying B2 multipart upload to free the storage.

Assembly is wrapped in a row-level lock so two clients racing the same destination can't both win. The first MOVE wins; the second gets a 409 conflict.

Audit trail

Every mutation through the sync facade lands a row in drive_activity_events with:

  • actor_type — for Drive Sync this is always api_token (the device password counts as a token-like actor).
  • mailbox_id if the actor is mailbox-scoped, NULL if account-scoped.
  • api_token_id linking back to the device password row.
  • drive_file_id or drive_folder_id (whichever applies).
  • event_type — one of file_uploaded, file_renamed, file_trashed, folder_created, folder_trashed, folder_renamed.
  • ip and user_agent from the request.
  • metadata — JSON with the size in bytes (for uploads), the new name (for renames), etc.

The same table feeds the delta cursor at /api/v1/drive/sync/changes, so a second client polling for changes sees mutations made over WebDAV.

Rate limits

Per device password, per hour:

  • 5,000 reads
  • 1,000 writes

Exceed either limit and the response is a 429 with a Retry-After header. The limit is per-password, not per-account, so multiple sync clients on the same account don't share a single bucket.

This is generous enough that no normal sync workload hits it — rclone running with the default --transfers 4 against an empty Drive will sit comfortably under both limits. Hitting 429 usually means a misconfigured script in a tight loop; the fix is to back off and check the loop, not raise the limit.

Feature flag

The whole sync surface is gated behind a single environment flag, DRIVE_DAV_ENABLED. When the flag is off (the default for new TrekMail deployments), the /dav/files/* routes return 404 and the Sync devices page in the dashboard does the same. This lets operators stage the feature without exposing it to users before they're ready.

White-label tenants inherit the flag from the platform — sync is on or off platform-wide, not per-tenant.

What's next

Related articles

Jump to nearby guides that continue the workflow.

We use cookies for essential functionality. No ads, no ad tracking.

Sign in to TrekMail

Access your dashboard, mailboxes and DNS.

or
or

Reset email sent

If an account exists for this email, we've sent password reset instructions.

By continuing, you agree to TrekMail's Terms and Privacy Policy.