Security
Last updated: May 2026
PayOwed handles sensitive credentials (email OAuth tokens, bank details, payment links) on behalf of merchants. This page describes how that data is protected. Every claim below maps directly to code in production.
1. Encryption
At rest
All OAuth tokens (Gmail, Xero, PayPal) and sensitive credentials are encrypted before storage using AES-256-GCM with a 32-byte key, 12-byte random IV per encryption, and a 16-byte authentication tag for tamper detection. The encryption key is stored as an environment variable, never in source code.
Bank account details (account number, routing number) are stored encrypted using the same cipher. Email bodies rendered for display in the portal use the client portal token for access control, not stored in plaintext.
In transit
All connections use TLS. The application is deployed on Vercel with automatic HTTPS and HSTS headers. Database connections use TLS via the Supabase connection pooler.
2. Authentication and access
- Clerk authentication handles user identity, session management, and JWT verification. No custom password storage.
- OAuth scope minimization: Gmail is requested with
send scope only (not full mailbox read). Xero with accounting.transactions for the specific operations needed. - API route protection: every authenticated endpoint verifies the Clerk session token. Unauthenticated access returns 401.
3. Token security
- Portal and dispute tokens:
Client.clientPortalToken and Invoice.disputeToken use 32 bytes of entropy (256 bits), generated by PostgreSQL gen_random_bytes(32), hex-encoded. - Demand letter tokens: 32 bytes of entropy (256 bits), generated by Node.js
crypto.randomBytes(32), hex-encoded with a dl_ prefix. - Timing-safe comparison: unsubscribe token verification uses
crypto.timingSafeEqual to prevent timing side-channel attacks. - HMAC-based unsubscribe tokens: derived from
HMAC-SHA256(invoice_token + client_email) with a server-side secret. Not guessable from the invoice alone. - Per-encryption random IV: each AES-256-GCM encryption uses a fresh 12-byte IV, preventing ciphertext analysis across encryptions of the same plaintext.
4. Bank account details
When merchants configure bank transfer as a payment option:
- Full account details are stored encrypted (AES-256-GCM)
- Reminder emails show only the bank name and last 4 digits of the account number
- Full details are accessible only through the authenticated client portal (gated by a cryptographically random portal token)
5. Webhook security
Inbound
- Stripe: all inbound webhooks are verified using
stripe.webhooks.constructEvent with the endpoint signing secret. Invalid signatures are rejected before processing. - Resend: email delivery webhooks are verified using Svix signature verification.
Outbound
- Merchant webhook deliveries are signed with
HMAC-SHA256 using the merchant's webhook secret - Failed deliveries are retried with exponential backoff (max 5 attempts)
- Webhook secrets are unique per merchant, generated at registration
6. Email handling
- Per-merchant OAuth: emails send from the merchant's own Gmail or Outlook account via OAuth, not from a shared sender. This means SPF, DKIM, and DMARC align with the merchant's domain.
- Multi-layer rate limiting: 50 emails/day cap (Gmail allows 500; we stay below burst-detection thresholds), 5 emails on first day, minimum 2-minute spacing between sends, plus a global queue that drains at a controlled rate.
- Bounce monitoring: automatic pause when bounce rate exceeds 5% of recent sends. 24-hour cool-off before resuming.
- CAN-SPAM compliance: physical business address footer in every commercial email, one-click unsubscribe via List-Unsubscribe header, and jurisdiction-aware frequency caps (AU, GB, US, CA, NZ).
7. Monitoring and incident response
- Sentry error tracking with comprehensive PII scrubbing: IP addresses, email addresses, OAuth tokens, session cookies, authorization headers, and monetary amounts are all stripped before any error data leaves PayOwed.
- Distributed cron lock via Upstash Redis with SET NX + TTL. Fail-closed: if Redis is unavailable, the cron skips rather than risking duplicate sends.
- Audit log: security-relevant actions (payments, disputes, login events) are logged with user ID, action type, resource, IP, and user agent.
8. Data location and retention
- Database: PostgreSQL hosted on Supabase (AWS us-west-2 region). Connection pooling via PgBouncer.
- Backups: Supabase provides automatic daily backups with point-in-time recovery.
- Account deletion: deleting your PayOwed account removes all invoices, clients, reminders, activity entries, and OAuth tokens. This is a hard delete, not a soft archive.
- Data export: available in dashboard Settings. Full account data exports as JSON; reports export as CSV.
9. Compliance posture
- CAN-SPAM (US) and CASL (Canada): physical address in every email, unsubscribe mechanism, no deceptive headers.
- Jurisdiction frequency caps: configurable per-merchant. Australia (ACCC): max 3/week, 10/month. UK (FCA): max 5/week, 15/month. US (FDCPA): max 7/week, 21/month. Canada: max 3/week, 7/month.
- GDPR: data subject rights are supported (access, export, deletion). Email tracking pixels are suppressed for clients in EU/UK jurisdictions when the client's country is set.
10. Reporting a security issue
If you discover a security vulnerability, please report it to security@payowed.com.
- We will acknowledge your report within 48 hours
- Critical vulnerabilities will be patched within 30 days
- We will not pursue legal action against good-faith security researchers
Please do not publicly disclose vulnerabilities until we have had a chance to address them.
See also: Privacy Policy