Security at a glance
The short version. Each item below links back to a detailed section further down.
TLS everywhere
Cloudflare Full SSL terminates TLS and re-encrypts to origin. All traffic, every endpoint.
Bcrypt + passkeys
Passwords hashed with bcrypt cost 12. WebAuthn passkey support (Touch ID, Windows Hello, hardware keys).
Short-lived JWTs
15-minute access tokens with 7-day refresh rotation. Family-based reuse detection revokes the whole chain on replay.
AES-256-GCM exports
Encrypted .dvnt project files use AES-256-GCM with PBKDF2-SHA256 (100,000 iterations). Client-side only — we never see the passphrase.
Parameterized SQL + Zod
100% parameterized queries via mysql2. Zod schemas validate request bodies on 12 POST/PUT endpoints.
Helmet + CSP
Helmet default headers (HSTS, X-Frame-Options, nosniff). Electron renderer runs under a strict Content Security Policy with an exact-provider allowlist.
Authentication
Single-factor password auth, phishing-resistant passkeys, and a short-lived JWT flow. Rate-limited and enumeration-resistant by design.
Passwords
- Hashed with bcrypt (cost factor 12) via
bcryptjs. Plaintext passwords never touch disk. - Length enforced at 6–72 characters (the upper bound is the bcrypt input limit).
- Comparison uses
bcrypt.compare()— constant-time, no short-circuit on wrong email vs wrong password. - Login failures return a single generic error. The server does not distinguish "no such account" from "wrong password."
POST /api/auth/login,/register, and/forgot-passwordare rate-limited to 20 requests per 15 minutes per IP.
Passkeys (WebAuthn)
- Passwordless login via Touch ID (macOS), Windows Hello, or hardware security keys. Implemented with
@simplewebauthn/server. - Credentials stored as public key + counter in the database. Counter-based replay protection increments on every use.
- Registration requires an existing authenticated session — passkeys can only be added to already-verified accounts. Users can register multiple passkeys.
JWT access + refresh
- Access token: HS256 JWT, 15-minute expiry, carries user id + email + tier. Verified on every authenticated request.
- Refresh token: Opaque 32-byte random token stored server-side as SHA-256 hash, 7-day expiry, rotated on every use.
- Reuse detection: Refresh tokens are issued in a family. If a revoked token in a family is replayed, the entire family is revoked and all sessions for that user are invalidated — standard indicator of a stolen token.
- The raw refresh token is never persisted — only its hash.
JWT_SECRETis validated at server startup: must be set and at least 32 characters, or the process exits before accepting traffic.
Cookie transport
- Tokens travel as cookies, not
Authorization: Bearerheaders. The web app receives__Host-dv_session(15-minute access token),__Host-dv_session_refresh(7-day refresh token), and__Host-dv_csrf(double-submit CSRF token) on every authenticated response. - All three are
HttpOnly(exceptdv_csrfwhich is read by the client to send back as a header — required for the double-submit pattern),Secure, andSameSite=Nonewith the__Host-prefix. The prefix forces no-Domain scoping, blocks sibling-subdomain cookie tossing, and is enforced by the browser independently of any server bug. - Cookies are scoped to the
drivant.comregistered domain. They travel across same-domain subdomains (app.drivant.com→drivant.com/api) to support web and Electron clients on different origins, but are never sent to third-party domains. - The Electron desktop app additionally receives the access + refresh tokens in the JSON response body (gated on
X-Electron-Client: 1) so the renderer can plant them into Electron's persistent cookie jar via IPC. Web clients get the user object only — the response-body tokens are suppressed for web requests. - All authentication endpoints (
/auth/login,/auth/refresh,/auth/verify-email,/auth/passkey/*,/auth/logout) carryCache-Control: no-store+Pragma: no-cacheso intermediaries don't accidentally retain credential payloads.
Email verification
- New accounts cannot log in until email is verified. Verification token is 32 bytes of
crypto.randomBytes, expires in 24 hours. - Registration is enumeration-resistant: existing emails return the same 200 response as new signups. No 409, no "already registered" hint.
- Resend-verification and forgot-password endpoints always return success regardless of whether the email exists.
Account suspension
- Admins can suspend accounts; the flag is checked in the auth middleware on every request. Suspended users receive
403 Account suspendedand cannot obtain new tokens.
Data handling
Where your data lives and how it gets there.
| Data | Storage | At-rest protection |
|---|---|---|
| Project data | Wasabi S3 (US), gzip-compressed with dvntz: prefix |
AES-256 at rest (Wasabi) + optional client-side AES-256-GCM on export |
| Driver photos | Wasabi S3, JPEG, 2 MB per-photo cap, label allowlist | AES-256 at rest (Wasabi) |
| Relational data | MySQL on our VPS | Daily mysqldump backups, gzip-compressed, written to Wasabi S3 under backups/. 30-day retention. |
| Payment information | Stripe — we never receive full card numbers | Stripe is PCI DSS Level 1; we hold a customer ID, not card data |
| Email (transactional) | Mailgun — verification, password reset, billing, trial expiry | TLS in transit. No marketing lists. No promotional sends to non-users. |
Project isolation
- Every project query is scoped by
user_idtaken from the verified JWT — never from request parameters. - S3 keys follow
users/{userId}/projects/{projectId}.json, withuserIdalways sourced from the JWT. PUT /api/projects/:idaccepts an optionallastUpdatedAttimestamp for optimistic concurrency — if the server has a newer record, the request is rejected with409 Conflictinstead of silently overwriting.
.dvnt file format
- Compressed: 8-byte DVNT header + gzip(JSON). ~90% smaller than raw JSON.
- Encrypted (optional): 8-byte header + 16-byte salt + 12-byte IV + AES-256-GCM ciphertext.
- Key derivation: PBKDF2-SHA256, 100,000 iterations. Fresh salt and IV are generated per encryption via
crypto.getRandomValues. - Encryption runs in the browser or Electron renderer via Web Crypto (
crypto.subtle). The passphrase never leaves the client.
GDPR & CCPA compliance
Rights we support today, backed by real endpoints — not a policy page with no teeth.
- Self-service account deletion.
DELETE /api/auth/accountrequires password confirmation, runs a transactional cascade across all user tables (projects, folders, telemetry, error reports, refresh tokens, passkeys, tickets, dispatch data, drivers, org memberships), and best-effort deletes S3 project and share objects. - Telemetry opt-out. Per-user flag, honored server-side — when opted out, no usage events are recorded.
- Data export. Projects export to
.dvnt, Excel, CSV, JSON, GPX, KML, or GeoJSON — you own your data and can walk away with it. - Data access and correction. Profile editable in Account settings. Project data editable in-app. For formal requests, use our data request form.
- Do-not-sell. We do not sell personal information — documented in our Privacy Policy in line with CCPA §1798.120.
- Record of Processing Activities (ROPA). A maintained, internal GDPR Article 30 record is available to customers on request via [email protected].
- Data Processing Agreement (DPA). Our standard, GDPR Art. 28 / SCC-aligned DPA is published in full at drivant.com/dpa. Org owners can request a counter-signed counterpart from inside the app — we return a counter-signed PDF within 5 business days.
Every subprocessor we use (Stripe, Mailgun, Mapbox, Wasabi, Cloudflare) operates under a DPA or equivalent contractual commitment covering GDPR-compliant data handling.
Electron desktop security
The desktop app follows Electron's current hardening guidance — renderer isolation, no Node, restricted file access.
| Setting | Value |
|---|---|
contextIsolation | true |
nodeIntegration | false |
sandbox | true |
enableRemoteModule | false |
| IPC surface | Exposed via contextBridge only, under a single window.drivant namespace |
| Navigation | will-navigate blocks everything outside localhost dev + file://. setWindowOpenHandler denies all popups. |
File I/O is sandboxed
- All file-read, file-write, and load-project IPC calls pass through a single
validateFilePath()gate that resolves the path, rejects.., and confirms it lives under Documents, Downloads, Desktop, or the system temp directory. - Anywhere else on disk throws
"File access denied — path outside allowed directories". store:setrejects prototype-pollution keys (__proto__,constructor,prototype).
Build integrity
- macOS: hardened runtime enabled, builds are notarized with Apple.
- Windows: code-signed installers.
- Auto-update metadata and binaries are served from our own Wasabi bucket over HTTPS.
Renderer CSP
Electron applies a custom Content Security Policy via onHeadersReceived. connect-src allowlists our own API, Mapbox, HERE, OSRM, Nominatim, LocationIQ, Stripe, and Wasabi — no wildcards to arbitrary origins.
Network security
Cloudflare edge
- DNS proxied through Cloudflare with SSL in Full mode — Cloudflare terminates TLS, re-encrypts to origin.
- Automatic L3/L4 + L7 DDoS mitigation.
- Static assets cached at the edge;
/api/*bypasses cache so responses are always fresh.
Server
- Nginx reverse-proxies to a single Node.js process manager (PM2) on
localhost:3100. - UFW firewall: only ports 22 (SSH), 80 (HTTPS redirect), and 443 (HTTPS) accept inbound traffic. The API port is not externally reachable.
- Express uses
trust proxy: 1soreq.ipreflects the real client IP behind the single reverse proxy — critical for accurate rate limiting.
Rate limiting
| Scope | Limit | Window |
|---|---|---|
| Auth (login, register, forgot-password) | 20 requests | 15 min / IP |
| Telemetry, error reports, waitlist | 500 requests | 15 min / IP |
| Public support tickets | 5 requests | 15 min / IP |
| Global | 1,000 requests | 15 min / IP |
HTTP hardening
- Helmet middleware applies defaults for HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, and Referrer-Policy.
- CORS is a strict allowlist:
https://drivant.complus local dev origins. No wildcard. - JSON body limit: 5 MB globally.
- Every request is assigned a 16-character hex request ID and logged to structured JSON logs (method, path, status, response time, user ID). Health checks excluded to cut noise.
WebSocket auth
- Dispatcher channels (
dispatch:{accountId}) require a valid JWT passed via query string, same secret as the REST API. - Driver PWA channels (
route:{token}) are authenticated by the opaque 32-byte route token. - 30-second ping/pong heartbeat. Unresponsive clients are terminated. 10 channels max per client, 64 KB payload ceiling.
Subprocessors
Every third party that touches customer data, what it does, and what it sees. Each one is named in our Data Processing Agreement; sub-processor changes are notified 30 days in advance.
| Subprocessor | Purpose | Data |
|---|---|---|
| Stripe | Subscription billing | Email, name, payment method (card tokens — we never see cards) |
| Mailgun | Transactional email | Email address, first name |
| Mapbox | Map tiles, geocoding, directions | Addresses, coordinates, tile requests |
| HERE Technologies | Commercial truck routing (Pro and above) | Route waypoint coordinates |
| OSRM | Default driving directions | Route waypoint coordinates |
| Nominatim (OpenStreetMap) | Geocoding fallback | Addresses |
| LocationIQ | Geocoding fallback (overflow) | Addresses |
| Wasabi | Project data, driver photos, database backups | Project JSON, JPEG photos, SQL dumps |
| Cloudflare | DNS, CDN, WAF, TLS termination | HTTP request/response metadata |
What we don't yet offer
We'd rather name the gaps than paper over them. Here's what we can't promise today.
- SOC 2. Not pursued yet. It's on the Fleetfully roadmap for when we have the revenue and headcount to do it properly rather than as a checkbox exercise.
- HIPAA BAA. Not available today. If you're in healthcare and need one, email [email protected] so we can understand whether the fit makes sense before you commit.
- SAML / OIDC SSO. Planned for our Fleetfully enterprise tier (Q3–Q4 2026). Not available on current tiers.
- Customer-managed encryption keys (BYOK). Not today. Data at rest is protected by Wasabi's AES-256 plus optional client-side AES-256-GCM on export.
- Audit log export. We log admin actions server-side but do not yet expose a structured, customer-facing audit log. Planned alongside Fleetfully.
- Account lockout on failed login. We rely on the 20-per-15-minute auth rate limit. A dedicated lockout mechanism is not implemented.
- Server-side token revocation list. Access tokens are short-lived (15 minutes) and refresh tokens support family-wide revocation on reuse — but there is no global JWT blocklist. A leaked access token is valid for up to its 15-minute lifetime.
- Public bug bounty. No formal program yet. Responsible disclosures are welcomed at [email protected] and we respond.
Contact and disclosure
Security reports, DPA requests, ROPA requests, and compliance questionnaires all go to a single inbox. We monitor it and respond.
- Email: [email protected]
- Responsible disclosure: Please give us a reasonable window to confirm and remediate before public disclosure. We'll credit you in release notes if you'd like.
- security.txt: A
/.well-known/security.txtfile will be published alongside our next release. In the meantime, the email above is the authoritative contact.