Skip to main content

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 renders table for analytics.
  • Image buffers uploaded via returnUrl are content-addressed (md5 of the spec) and stored with Cache-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.
  • logoUrl in card compositions is validated to HTTPS in CardHeaderSchema (lib/validation/chart-spec.ts).
  • Response bodies include X-Request-Id to 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 returnUrl responses (opt-in private buckets).