packages/tenancy-rls's buildTenantPolicySql emits ENABLE ROW LEVEL SECURITY plus FORCE ROW LEVEL SECURITY on every tenant table, so the policy binds the table owner too, not just other roles. The sole entry point, withTenant, opens a transaction, drops to the unprivileged app role, and binds the account id into a Postgres GUC (app.current_account) that every policy reads; a code path that forgets withTenant has no GUC bound and the table returns nothing. A one-time role pre-flight (assertRoleNotPrivileged) refuses to run if that role is ever a superuser or BYPASSRLS, since either would silently no-op FORCE ROW LEVEL SECURITY. Cross-tenant isolation is a CI test, not a convention.
Fail-closed by construction.
The same posture Caisson generates for your app governs this site: deny by default, prove it with code, claim nothing we do not ship. Caisson generates audit evidence, it is not the auditor.
$ curl -sI https://caisson.sh strict-transport-security: max-age=63072000; includeSubDomains; preload x-content-type-options: nosniff x-frame-options: DENY referrer-policy: strict-origin-when-cross- origin permissions-policy: geolocation=(), microphone=(), camera=() content-security-policy: default-src 'self'; …
The security your app inherits on day one.
These are product features, wired and tested into the codebase Caisson generates, not services we run on your behalf. You own the source and the evidence.
packages/kernel's ssrf.ts stops DNS rebinding on every buyer- or config-supplied URL (the alerting webhook transports and the AI-Production provider baseUrl both route through it. assertSafePublicUrl rejects non-https, credentials-in-URL, and a literal private/loopback/link-local/metadata host at the config boundary; assertResolvedHostPublic then resolves the hostname and re-checks every returned A/AAAA record against the same denylist immediately before the outbound fetch, so a public name that DNS-rebinds to 127.0.0.1 or 169.254.169.254 is caught where a literal-only check can't see it. ssrfGuardedFetch forces redirect: "error") only the original host is re-checked, so a followed redirect could otherwise carry the request past the guard.
Evidence buckets enable S3 Object Lock. The default is GOVERNANCE mode, inside the retention window an object cannot be overwritten or deleted by an app bug or an ordinary operator, though a caller holding s3:BypassGovernanceRetention can still override it. COMPLIANCE mode is available as an explicit, irreversible opt-in (production-only, gated behind irreversibleComplianceOptIn) for retention even the AWS account root cannot shorten.
Each audit row commits SHA-256 over the previous hash plus its own payload. Tampering with any historical row breaks every link after it, and the break is detectable, provable, and exportable.
packages/kernel's crypto.ts is the one home for secret comparison: safeEqualFixed converts both sides to equal-length buffers and runs node:crypto's timingSafeEqual for session tokens and HMAC outputs of known length; safeEqualVariable SHA-256-hashes both sides first for variable-length values like an admin-email allowlist entry, because a raw variable-length timingSafeEqual throws on a length mismatch and leaks a boolean through the catch. verifyAllowlisted scans every allowlist entry with no early return, so the timing never reveals which entry matched.
Sensitive columns are sealed with a data key derived per tenant via HKDF-SHA256. A leaked tenant key exposes one tenant, never the table; rotating the root re-derives every key without a re-encrypt scan.
How caisson.sh itself is secured.
A dynamic app widens the attack surface, we keep it deliberately scoped and document exactly what ships.
caisson.sh runs as a Next.js standalone Node server on Railway, backed by Postgres for the buyer dashboard, billing, and checkout. Marketing and docs pages still render statically at build time; only the dashboard, checkout, and forms are dynamic, and every authed route runs the same fail-closed tenant isolation the product ships, no secrets in the client bundle.
apps/admin renders cross-tenant business data, so an app-wide middleware validates the Cf-Access-Jwt-Assertion token's signature, audience, and issuer against the admin Access app's own JWKS before any route runs, denying with a 403 on any failure or misconfiguration. Both CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD must be set before the app serves a single route.
Every response carries HSTS with preload, X-Content-Type-Options: nosniff, a strict Referrer-Policy, a closed Permissions-Policy, and a tightened Content-Security-Policy. Framing is denied outright on every page, with one exception: the /demos/* embed surface, which a module page frames on this same origin and which no other site can frame.
Analytics run through Plausible, no cookies, no cross-site identifiers, no consent banner because there is nothing to consent to. Fonts ship from our own origin via next/font, so font-src stays locked to 'self' with no third-party font CDN in the trust surface.
The ask-AI and waitlist routes both validate input with Zod .strict() (unknown fields rejected) and gate on a Cloudflare Turnstile token before any request reaches the model or the mailing list. makeTurnstileVerifier fails closed on the ask route; the waitlist route also drops bots via a honeypot field.
A machine-readable policy lives at /.well-known/security.txt (RFC 9116). Report anything you find to [email protected], we read it.
The policy that ships, including what is not yet locked down.
We state CSP residuals plainly rather than imply a tighter policy than we run. Trust is the product; over-claiming it would defeat the point.
$ curl -sI https://caisson.sh | grep -i '^content-security-policy' content-security-policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data: https://*.paddle.com; font-src 'self'; style-src 'self' 'unsafe-inline' https://*.paddle.com; script-src 'self' 'unsafe-inline' https://plausible.io https://cdn.paddle.com https://challenges.cloudflare.com https://us-assets.i.posthog.com; frame-src 'self' https://*.paddle.com https://challenges.cloudflare.com; connect-src 'self' https://plausible.io https://*.paddle.com https://challenges.cloudflare.com https://us.i.posthog.com https://us-assets.i.posthog.com
script-src and style-src still allow 'unsafe-inline'. Next inlines its own hydration bootstrap with no per-request nonce under the App Router, so those inline tags cannot be hash- or nonce-gated without breaking hydration. Beyond 'self' the policy allows exactly three third parties, each scoped to the surface that uses it: Plausible for cookieless analytics, Paddle (cdn.paddle.com for the checkout script, *.paddle.com for its overlay iframe and API), and Cloudflare Turnstile (challenges.cloudflare.com) for the invisible bot check on the Ask-AI assistant. Nothing wider. Tightening the inline residual to per-script hashes is a tracked follow-up, not a shipped claim.
Caisson generates evidence. It is not an auditor.
Caisson ships the technical controls a framework asks for and generates the evidence pack. The audit, and your organizational controls, stay yours.
We never imply Caisson is SOC 2 or HIPAA certified. A codebase cannot be. It maps the live RLS policies, the SSRF guard, WORM retention, and the audit-chain proof to named controls so you can hand an auditor the evidence, not a screenshot. The technical-versus-administrative line is drawn on purpose, and we keep it visible.
The questions procurement asks first.
Is Caisson SOC 2 or HIPAA certified?
Does this site set tracking cookies?
What stops a DNS-rebinding attack against a webhook or provider URL I configure?
How is the admin dashboard protected if it renders every tenant's data?
How do I report a vulnerability?
Found something? Tell us.
We publish a machine-readable policy and read every report. No bounty program yet, the report still matters.