Zero-access encryption by design
Your synced profile data is encrypted on your device before it ever reaches a server, so the server stores only encrypted blobs it can never read. Calendar app connections are a documented plaintext exception, scoped to your Calendar inbox and read-only subscriptions.
How encryption works
Graspee uses a client-side key hierarchy. Your password and a random salt are run through PBKDF2 with 200,000 iterations to derive a Key Encryption Key (KEK). The KEK encrypts a randomly generated Data Encryption Key (DEK). The DEK encrypts all your data using AES-GCM-256. Your password never leaves the browser.
What zero-access means
The server never sees your plaintext data, your password, or your encryption keys. Even with full database access, the server operator cannot read your notes, tasks, transactions, or other synced content. If you enable sync, the server relays encrypted blobs between your devices but cannot decrypt them. Calendar app connections are a documented exception: read-only subscription feeds are plaintext artifacts that external calendar apps must be able to read, and Quick Add events sent to your Calendar inbox are stored in plaintext while pending so the unlocked browser can import them. After import, ignore, expiry, or tombstone the staged plaintext is scrubbed.
Session security
Your DEK is encrypted under a temporary Session Wrapper Key stored in sessionStorage. It survives page refreshes but is cleared when you close the browser. Sessions use HttpOnly cookies with SameSite and Secure flags. CSRF protection is enforced via origin checking. Rate limiting protects authentication endpoints.
Sync encryption
Cloud sync uses Yjs CRDT documents encrypted client-side. The WebSocket transport carries only encrypted payloads. State vectors and updates are validated server-side for size but never decrypted. Compaction uses optimistic locking to prevent data loss.
Bank connection security
Provider credentials such as Plaid access tokens, Flinks login IDs, or MX user/member GUIDs are stored in your encrypted local shard, never in the server database. The server acts as a pass-through: it relays provider API calls and may hold sync payloads briefly in memory, but stores no durable provider data. HMAC ownership proofs bind tokens to your account statelessly. A separate connection link ledger holds non-reversible HMAC fingerprints of provider connection IDs to enforce per-user link allowance limits. Flinks and MX are only available where the operator has enabled them.
Calendar app connections exception
Calendar app connections are an intentional, limited plaintext exception to zero-access. Read-only subscription artifacts are plaintext because external calendar apps must be able to read .ics, and Quick Add events from your calendar app are stored in plaintext in your Calendar inbox while pending so the unlocked browser can import them. The server keeps that staged inbox plaintext while pending and the published read-only subscription artifacts; for credentials and bearer tokens, only hashes are kept on the server, and the live subscription token is held in encrypted local storage on the unlocked browser. Pending events expire after 7 days, and staged plaintext is scrubbed after browser import, ignore, expiry, or tombstone. If a subscription URL leaks, rotate it from Settings to invalidate every existing subscriber.
Subscriptions and payments
Subscription payments are processed by Stripe via a hosted checkout page. Your card number, expiry, CVC, billing address, and Stripe Link credentials never enter Graspee's frontend or backend; they live exclusively in Stripe's PCI-DSS Level 1 environment. Graspee stores the Stripe-issued customer and subscription identifiers needed to associate the subscription with your account, plus a checkout-correlation row used to safely retry an interrupted Checkout. The first time you subscribe, your account email is sent to Stripe (as customer_email) and is persisted inside the correlation row's request snapshot so a retried Checkout can be replayed under the same idempotency key; once Stripe has issued a customer identifier, only that identifier is sent on later attempts. The email-bearing request snapshot is always cleared 24 hours after the attempt is created, whether the attempt is still open, has completed or failed, or has parked in delayed-payment settlement. The correlation row itself is then deleted at 90 days; delayed-payment attempts that have not yet been settled by Stripe persist (without the email) up to that horizon so the eventual succeed/fail event can reconcile, and are then deleted with the same retention as completed/failed attempts. Every billing-state change is driven by signed Stripe webhooks: the webhook signature is verified against the raw request body with HMAC-SHA256 and a 5-minute timestamp tolerance before any account state is touched. Paid access is never granted from a checkout URL, query string, or client response; it follows only from a reconciled subscription. Raw webhook payloads are not persisted on the server; the audit log keeps only event identifiers, types, and hashes of related Stripe object ids.