Security
Last updated: 18 April 2026
Kandan brokers OAuth tokens to your inbox. We take that trust seriously. This page describes concretely how we protect your data. If you find a security issue, please report it responsibly.
Encryption in transit
- All public endpoints are served over HTTPS (TLS 1.2+). HTTP requests are redirected to HTTPS.
- Internal traffic between the application server and managed services (database, queue, email) is kept inside Hetzner's private network or uses TLS.
Encryption at rest
- OAuth tokens (access + refresh) are encrypted column-level with Laravel's
encryptedcast (AES-256-CBC + HMAC) using an application key that is never committed to source control. - Passwords are hashed with bcrypt (cost 12). Plain passwords are never written to logs or the database.
- Plugin-level secrets (webhook signing keys, etc.) use the same encrypted-cast pattern.
- The application's
APP_KEYitself is stored encrypted in our committed production env file using dotenvx (libsodium sealed boxes). The private decryption key is held separately in a password manager and supplied only at deploy time.
How we handle OAuth tokens
- We request the minimum scopes needed for the integration to function. Scope requests are shown to you at the Google / GitHub consent screen.
- Refresh tokens are stored server-side and used only to re-mint short lived access tokens. They never leave our backend — not to your browser, not to the desktop app, not to any third party.
- When our circuit breaker detects that a token has been revoked or refuses refresh, we stop polling, notify you, and require you to reconnect. Tokens are wiped from the database when you disconnect a source or delete your account.
- You can revoke Kandan's access at any time from Google Account Permissions or GitHub Applications.
Authentication & sessions
- First-party auth uses Laravel Sanctum personal access tokens, scoped to a device, with configurable expiry.
- The desktop app uses an OAuth 2.0 PKCE flow (no client secret required on the device) to obtain its own access token. Verifier and state values live in session storage only for the duration of the flow.
- Session cookies are marked
Secure,HttpOnly, andSameSite=Lax. Session data is encrypted at rest. - Active sessions are visible to you under "Account → Sessions" and can be revoked individually.
Webhooks
- Generic webhooks require a per-install token embedded in the URL. Rotating the token is a one-click action.
- GitHub webhooks are verified against a per-install HMAC secret before processing.
- Gmail push uses Google Cloud Pub/Sub; the receiving endpoint verifies the subscription and validates the JWT in the
Authorizationheader.
Hosting & infrastructure
- Application servers: Hetzner (Germany, EU). No application data leaves the EU during normal operation.
- Marketing site and app shell: Cloudflare Workers with static assets, EU-preferred edge.
- Transactional email: Resend (EU region). Only recipient address and message content of transactional emails we originate are sent — never content from your connected sources.
- Error tracking: Sentry (de.sentry.io, EU). PII scrubbing is enabled by default; stack traces and metadata are retained for 90 days then purged.
Access controls
- Production database access is limited to the application user and a small number of engineers with SSH keys. No shared credentials.
- Two-factor authentication is enforced on all third-party accounts with production access (Cloudflare, Hetzner, GitHub, Sentry, Resend, Google Cloud).
- Engineers never read customer notification content except on explicit written request from the account owner to investigate a support issue.
What we do not do with your data
- We do not sell, rent, or share your data with advertisers.
- We do not train AI/ML models on notification content or Google user data.
- We do not send notification content to any third-party analytics service.
- We do not store full message bodies or attachments from Gmail.
Software supply chain
- Dependencies are pinned in lockfiles (
pnpm-lock.yaml,composer.lock) and reviewed before upgrade. - CI runs tests, type-checks, and the Laravel Pint formatter on every push. Deployments are gated on a green build.
- Production secrets are injected at deploy time and never written to image layers or logs.
Backups
Automated daily database snapshots are retained for 14 days, encrypted at rest, and stored in the same EU region as the primary database. Restores are tested periodically.
Reporting a vulnerability
If you think you've found a security issue, please email security@usekandan.com. We aim to acknowledge within 2 business days and to ship a fix or mitigation as quickly as the issue warrants.
Please:
- Give us reasonable time to respond before publicly disclosing.
- Avoid privacy violations, data destruction, and service disruption during your research.
- Use only your own accounts (or accounts you're explicitly authorized to test).
We do not currently run a paid bug bounty, but credit in release notes is available on request.