Security
Threat model
Assets we protect:
- Customer’s OpenAI API key (monetary value if stolen)
- Customer’s database credentials (data breach risk)
- OAuth refresh tokens for Shopify, Airtable, Google Sheets (continuous access)
- End-user voice audio (transient, not stored)
- End-user conversation transcripts (optionally stored 30 days)
Adversaries we defend against:
- Abuser — tries to run up our OpenAI bill via rate-limit bypass
- Snooping site owner — tries to read another tenant’s data
- Malicious end-user — tries to prompt-inject the AI into exposing other users’ data
- Network adversary — MITM on widget ↔ API or widget ↔ OpenAI
- Compromised dependency — npm supply chain attack
Controls summary
| Control | What it protects |
|---|---|
| CORS per registered domain | Stolen script tags can’t work from another site |
| AES-256-GCM encryption of all customer secrets | DB breach doesn’t reveal OpenAI keys / DB creds |
HMAC-SHA256 request signing on /query | Non-widget callers can’t abuse /query |
| Ephemeral OpenAI tokens | Leaked token damage bounded to ~2 hours |
| Read-only adapters | Malicious AI output can’t mutate your data |
| Identifier whitelisting | AI can’t query fields you didn’t expose |
| Parameterized values | SQL / NoSQL / GraphQL injection impossible |
| Rate limiting (per-IP + per-site) | Brute force and abuse patterns contained |
| Zod input validation | Malformed payloads rejected at the boundary |
Rate limiting
| Endpoint | Per IP | Per Site ID | Burst |
|---|---|---|---|
/v1/*/token | 60/min | 200/min | 20/sec |
/v1/*/query | — | 300/min | 30/sec |
/v1/*/describe-image | — | 30/min | 5/sec |
/v1/*/analytics/event | — | 600/min | 50/sec |
/v1/auth/* | 20/min | — | 5/sec |
Backed by Redis sliding-window counters. Exceeded → 429 with Retry-After.
Secret encryption
- Algorithm: AES-256-GCM
- Master key: 256-bit random, stored in Cloudflare Secrets or AWS KMS — never in DB
- Per-record IV: 96-bit random, stored alongside ciphertext
- AAD:
site_id, to prevent cross-tenant ciphertext substitution - Format:
{iv_base64}:{ciphertext_base64}:{tag_base64}
Master key rotation: quarterly, via re-encrypt job. Decrypt operations are audit-logged.
Request signing (widget ↔ API)
Every /query call includes X-Spelo-Signature: sha256=<hmac> over the body, using the session’s signing secret. The API verifies against the session’s secret.
Prevents someone scraping spelo.js and POSTing /query with another site’s ID.
Database read-only enforcement
Three layers:
- Connection-level — customers create read-only DB users; documented in every adapter’s setup guide.
- Query-level — adapters only emit
SELECT,find(),GET. No write paths exist in code. - Identifier whitelist — table / column / field names must appear in
filterable_fields/searchable_fields/display_fields.
Values are always parameterized ($1, ?, bind variables) — never string-concatenated.
OpenAI key isolation
- Customer enters key once in dashboard
- API encrypts and stores it
- Widget never receives the key
- Widget receives ephemeral session tokens (2h TTL)
- Leaked ephemeral → damage capped at ~2 hours of rate-limited OpenAI usage
Alternative: “Spelo managed AI” — we use our key and bill the customer.
OAuth token handling
- Encrypted at rest (AES-GCM)
- Scoped to read-only where the provider supports it
- Auto-refreshed before expiry (background job)
- Revocable from dashboard (calls provider’s revoke endpoint)
Account deletion revokes — not just deletes — OAuth tokens.
Input validation
Every incoming request validated with Zod:
site_id:[a-z0-9]{8,32}- URLs:
https://(orhttp://localhostin dev) - Filter values: typed against CollectionConfig
- Form inputs: length + pattern checks
Content Security Policy — recommended
Content-Security-Policy: script-src 'self' https://spelo.ai; connect-src 'self' https://api.spelo.ai https://api.openai.com wss://*; media-src 'self' blob:;Egress IPs
Customer firewalls often want to allow only specific IPs. Our egress CIDRs:
us-eastprimary —44.198.24.X/29us-westfailover —52.53.72.X/29
The live authoritative list is at spelo.ai/security/ips.
Incident disclosure
If we experience a security incident affecting customer data:
- Notify affected customers within 72 hours (GDPR-aligned)
- Publish a post-mortem at spelo.ai/incidents within 7 days
- Rotate affected secrets immediately
Penetration testing
Annual third-party pen tests; reports available under NDA to enterprise customers. Contact security@spelo.ai.
Bug bounty
Responsible disclosure is welcome. Email security@spelo.ai with reproduction steps. Rewards scale with severity and novelty.
Compliance roadmap
- DPA template
- Privacy policy + ToS
- SOC 2 Type I (Q2)
- SOC 2 Type II (Q4)
- HIPAA BAA (on request for healthcare customers)
- ISO 27001 (post Series A)