Security posture
A plain-English summary of how Chart-Output handles authentication, signing, data, transport, and rate limits. Every claim below is backed by code or a migration file you can inspect.
Summary
- API keys are never stored in plaintext. Only SHA-256 hashes are persisted.
- Webhooks are signed with HMAC-SHA256 over the raw body and verified with constant-time comparison.
- All Supabase tables have row-level security enabled; users can only read their own resources.
- Logos referenced in card headers must be HTTPS URLs; HTTP is rejected at validation time.
- Per-plan rate limits are enforced with Upstash Redis on every request.
- Trial accounts are PNG-only; output-format and PDF gating happens before rendering.
API keys
Keys are issued with a prefix that encodes the environment (pk_live_ or pk_test_) and a random suffix. Only the SHA-256 hash of the suffix is stored in the api_keys.key_hash column (see migration 001_initial_schema.sql). The plaintext is shown once at creation and never again — if you lose it, you rotate.
Authentication is checked on every request in lib/middleware/auth.ts. Keys can be supplied via the Authorization: Bearer <key> header or the ?key= query parameter for email embeds.
Webhook signing
Every webhook delivery carries an X-Signature header — an HMAC-SHA256 hex digest of the exact JSON body using your per-webhook secret. Verify it on your end with a constant-time compare before trusting payload contents. Reference implementation and a Node.js verification snippet are in Webhooks guide.
The secret is shown once at webhook creation. Losing it requires creating a new webhook and retiring the old one. Events emitted today: render.success, render.failed, and render.complete (async jobs).
Row-level security
Every tenant-scoped table has Postgres row-level security enabled. Users can only read or write rows keyed to their own auth.uid(). Service-role access is restricted to server-side code paths and never exposed to the browser. You can audit the policies directly in supabase/migrations/*.sql; RLS has been on for every tenant table since migration 001.
Data retention
- Render metadata (chart type, dimensions, latency, success/failure) is retained in the
renderstable for analytics. - Image buffers uploaded via
returnUrlare content-addressed (md5 of the spec) and stored withCache-Control: public, max-age=31536000, immutable. They are never overwritten. - AI render inputs (descriptions, attached data) are not persisted beyond the render itself; they are sent to the model and discarded. Only the validated output spec is optionally logged.
- Incidents on the status page show the trailing 90 days.
Transport & URL validation
- All production API traffic is HTTPS-only. HTTP-only clients are rejected at the edge.
logoUrlin card compositions is validated to HTTPS inCardHeaderSchema(lib/validation/chart-spec.ts).- Response bodies include
X-Request-Idto correlate client errors with server logs.
Rate limiting
Per-key sliding-window rate limiting runs on Upstash Redis (lib/middleware/rate-limit.ts). Limits are per plan and surfaced in the response as X-RateLimit-* headers. Exceeding the limit returns 429 Too Many Requests; nothing is silently queued.
Responsible disclosure
Email security@chart-output.com with a description, reproduction steps, and the potential impact. We acknowledge within two business days and will coordinate a fix before public disclosure.
Roadmap
- SOC 2 Type II — in planning for the next fiscal year.
- Third-party penetration test — once the AI surface area stabilizes.
- SAML SSO & SCIM — Enterprise-tier feature; see the Enterprise waitlist.
- Signed, short-TTL URLs for
returnUrlresponses (opt-in private buckets).