Zubby Security & Compliance
How Zubby AI protects multi-tenant ecommerce data — what we encrypt, how we authenticate, what we verify, what we refuse to accept from the wire. Every claim on this page links to the file in the codebase that implements it. SOC 2 Type I in progress with audit completion targeted for 2026.
Published 2026-05-19
At rest
AES-256-GCM
AAD-bound per tenant
In transit
TLS 1.3
HSTS preloaded
Passwords
scrypt
Constant-time verify
In progress
SOC 2 I
2026 audit window
Security architecture in one paragraph
Zubby AI is a multi-tenant ecommerce AI sales agent. Tenancy is enforced end-to-end by a store_id scope on every row and every query, with no cross-tenant query path in application code. Per-tenant secrets are encrypted at rest with AES-256-GCM using AAD-bound ciphertexts. The edge proxy verifies widget keys and Shopify embedded session tokens before any route handler runs. Webhook ingress is HMAC-verified and idempotent. Outbound HTTP from user-controlled URLs passes through an SSRF guard. Image uploads are magic-byte sniffed. Passwords are scrypt-hashed and verified in constant time. Sessions are NextAuth v5 JWTs with hard-invalidation on credential change. Sentry instruments Next.js (server, edge, client) and the BullMQ worker with PII scrubbing applied before capture. Every dashboard action emits an audit event with actor, action, target, and timestamp.
Encryption at rest — AES-256-GCM with AAD binding
Every per-tenant secret Zubby stores — Shopify access tokens, WooCommerce plugin tokens, third-party connector credentials (Klaviyo, Mailchimp, HubSpot, Segment, channel adapters), admin control-plane secrets (Stripe, Resend) — is encrypted with AES-256-GCM. The ciphertext format is enc_v2:<iv>:<authTag>:<ciphertext>. Each ciphertext is bound to an Additional Authenticated Data (AAD) context derived from (tenant, column, provider, field) via crypto.createHash("sha256").
The AAD binding is what makes this stronger than naive encryption. An attacker who obtains database write access cannot transplant a ciphertext from store A's Klaviyo row into store B's row to decrypt it — the AAD context does not match, so the GCM auth tag verification fails. The same protection applies across column boundaries: a stored Stripe webhook secret cannot be promoted into the Stripe secret-key column to read it under a different ACL.
The encryption key is derived from DATA_ENCRYPTION_KEY via SHA-256. Production startup fails if the variable is missing or equals AUTH_SECRET — the two secrets must be separable so credential rotation cannot leak data-encryption material. Legacy enc_v1 ciphertexts (no AAD) continue to decrypt for backward compatibility; the next write rolls the blob forward to enc_v2.
Password hashing and credential comparison
- Passwords. Hashed with
crypto.scryptusing a 16-byte random salt and a 64-byte derived key. Stored assalt:hex_derived_key. Verification usescrypto.timingSafeEqualagainst the derived key buffer. - API keys. Generated with crypto-grade randomness, scrypt-hashed at rest, verified in constant time. Per-scope permissions and per-scope rate limits limit blast radius from a compromised key.
- WooCommerce plugin tokens. Constant-time compare against the decrypted
stores.access_tokenciphertext. Implementation insrc/lib/woo-plugin-auth.ts. - Widget keys. Constant-time edge-safe verification in
src/lib/widget-auth-edge.ts. The proxy injects the resolved tenancy headers downstream so route handlers do not trust client-supplied store IDs.
Sessions, identity, and access control
- NextAuth v5 JWT sessions with hard-invalidation on credential change. A password rotation or admin-issued session revocation terminates every outstanding JWT for that subject immediately, not at next refresh.
- Multi-provider sign-in. Credentials (email + password), Google OAuth, and Resend magic-link email. Account-merge guard rails: a Google sign-in cannot silently absorb a credentials-protected account with the same email — the owner must sign in with credentials first and explicitly link Google from settings.
- Workspace RBAC. Owner, admin, agent, viewer roles gate every dashboard action. Invites are token-based, single-use, and audit-logged.
- Audit logs. Every dashboard action — invites, role changes, integration credential rotation, journey publish, knowledge upload, erasure request — emits an audit event with actor, action, target, ISO timestamp, and IP hash. Retention 24 months.
Tenant isolation by store_id
Every row in every table is scoped to a store_id. There is no cross-tenant query path in application code — the pgvector embedding index, conversation logs, customer records, journey state, AI tool-call logs, and outbound-webhook subscriptions are all keyed on store_id. Drizzle ORM queries always include the scope as a WHERE predicate.
The widget cannot start a conversation without an x-widget-keyheader verified against the store's widget key. The verification runs at the edge in src/proxy.ts and uses constant-time compare. On success, the proxy injects x-store-id and x-widget-key-id headers downstream so route handlers receive the resolved tenancy from a server-trusted source — never from the client.
Admin tooling is the only surface that can read across tenants. It is gated by workspace role, MFA, audit-logged on every cross-tenant query, and rate-limited per admin user.
Webhook verification and idempotency
- Shopify webhooks — HMAC-SHA256 against the app secret, verified with
crypto.timingSafeEqual. App-bridge headers are validated. The handler is idempotent: a replayed delivery with the samex-shopify-event-idis a no-op. - WooCommerce webhooks — plugin-signed token compared via constant-time helper against the decrypted
stores.access_token. - Stripe webhooks — official
stripe-signatureheader verification usingStripe.webhooks.constructEvent. Idempotent handlers keyed by Stripe event ID. Replays cannot double-process refunds or subscription transitions. - Twilio webhooks — signature header verification.
Webhook ingress runs through proxy-enforced rate limits (500 req / s per tenant bucket, Upstash-backed) so a flooded webhook source — including a misconfigured Stripe replay — cannot starve other tenants' processing.
Edge proxy and rate limits
src/proxy.ts matches /api/v1/*, /app/*, and /admin/*. Three responsibilities:
- Rate limiting for
/api/v1/*via Upstash Redis. Widget: 20 req / 10s. Widget chat: 6 req / 30s per key plus IP. Webhooks: 500 req / s per tenant bucket. In production, missing Upstash configuration returns 503 — we never silently disable. - Widget authentication. Every
/api/v1/widget/*request must carry anx-widget-keyheader verified against the store's widget key. On success, the proxy injectsx-store-idandx-widget-key-idheaders downstream. Routes trust those headers — they originate only inside the proxy. - Shopify embedded session tokens.
/app/*requests withAuthorization: Bearer <jwt>are HMAC-verified againstSHOPIFY_API_SECRET, audience-checked againstSHOPIFY_API_KEY, and injectx-shop-domain.
Application-layer defenses
- SSRF protection via
assertSafeOutboundUrl. Every outbound HTTP from a user-controlled URL (search-by-image, knowledge import, Loox pull, Klaviyo redirector, catalog-sync worker) is filtered. Blocks RFC 1918, link-local, loopback, AWS / GCP / Azure metadata addresses, with DNS rebinding mitigation. - Magic-byte upload sniffing. Image uploads to the widget, knowledge base, and product catalog are validated by parsing the actual file header — not the client-declared MIME type. Allowed: JPEG, PNG, WebP, GIF. Anything else rejected at ingest.
- Cloudflare Turnstile gates conversation start on suspicious traffic patterns. Configurable per store; defaults to enabled for new workspaces.
- Public API key authentication with per-scope permissions (read:catalog, write:conversations, read:orders, write:journeys). Keys hashed at rest. Constant-time verification. Per-scope rate limits.
- CSRF protection on state-changing dashboard actions via origin-checked POST handlers and double-submit cookie tokens on session-aware routes.
- Content Security Policy with strict
script-srcand a nonce-based allowlist. Marketing pages additionally pinframe-ancestorsto prevent clickjacking.
Operational security
- 99.9% uptime SLO on the merchant dashboard and widget chat API. Synthetic monitoring runs every 60 seconds. Real-time status at /status.
- Sentry-instrumented runtimes. Server, edge, client, and the BullMQ worker all report errors with PII scrubbed before capture.
- Dead-letter queue replay. Failed background jobs land in a dedicated DLQ with merchant-visible replay tooling in the dashboard.
- Annual third-party penetration testing of the public API and dashboard. Most-recent summary available under NDA.
- Quarterly internal access reviews of production systems. Admin-level privileged access requires MFA and is audit-logged on every action.
- Disaster recovery. Documented in DEPLOY.md. Managed Postgres point-in-time recovery; encrypted off-site snapshots daily. RTO 4h, RPO 1h. DR restore drills quarterly.
GDPR erasure pipeline
Privacy-rights fulfilment is a first-class background worker, not a ticket queue. The endpoint /api/v1/merchant/gdpr/delete accepts a shopper identifier and emits a fanout job in the gdpr-compliance queue: delete conversations, pgvector embeddings, journey state, customer record, AI tool-call logs, AI-generated drafts, and CDN-cached avatars. The job runs with 5 retries on exponential backoff so an external API hiccup cannot drop a legal obligation.
Shopify webhook callbacks for data-request, data-erasure, and shop-redact route through the same worker. WooCommerce equivalents exposed by the plugin do the same. Audit-logged on every step.
Responsible disclosure
If you believe you have found a security issue, email security@zubbyai.com with a concise description, the affected URL or component, and reproduction steps. Encrypt sensitive details with our PGP key on request. RFC 9116 security.txt is published at /.well-known/security.txt.
Please do not publicly disclose the issue before coordinating a fix with us.
What we commit to
- Acknowledge your report within 2 business days.
- Provide a timeline to remediation within 5 business days for valid issues.
- Credit you publicly once the fix has shipped, with your permission.
- Not pursue legal action against good-faith research that follows this policy.
Scope
- The hosted Zubby AI SaaS application at zubbyai.com.
- The official Shopify and WooCommerce plugins.
- Public APIs at
/api/v1/*and webhook endpoints. - The embeddable widget.
Out of scope: third-party services we integrate with (report to those vendors directly), social-engineering, denial-of-service, physical security, and reports from automated scanners without a working proof-of-concept.
Bug bounty
We currently run an invite-only bounty program with rewards based on impact. After a valid report, we will evaluate and reach out with an offer if eligible.
Contact
- Security: security@zubbyai.com
- Privacy / DPO: privacy@zubbyai.com
- Legal / DPA: legal@zubbyai.com
- AI safety: aisafety@zubbyai.com
Bring your security team. We will be ready.
DPAs, EU SCCs, sub-processor lists, SOC 2 readiness letters, pen-test summaries, and a Transfer Impact Assessment are available on request — typically within 24h. Security questionnaire turnaround is 5 business days on Pro and Enterprise.