Security model
Bailey’s job is to host other people’s code and collect other people’s data safely. This page explains the guarantees that make that defensible. None of it requires anything from you; it’s how the platform is built.
Site isolation (A never reads B)
Section titled “Site isolation (A never reads B)”- Every site’s files are opened through a kernel-level root jail (
os.Root). A crafted URL (.., an absolute path, a symlink) physically cannot escape one site’s directory. Isolation isn’t a path-cleaning heuristic; it’s enforced by the OS. - The store applies the same rule to data: every read/write checks that the caller’s token targets the same app it’s acting on, as the first authorization step. One site can never address another’s namespace.
- Ownership in the control plane is stamped server-side from your verified login; the client never asserts who it is, so it can’t claim someone else’s project.
A keyless in-page API, made safe
Section titled “A keyless in-page API, made safe”The SDK ships no secret in your page. Instead, writes pass a chain of checks (in order):
- Token signature + TTL: minted server-side, ~10 min lifetime.
- App match: the token’s app must equal the path’s app.
- Kill-switch: a disabled site rejects writes even with a still-valid token.
- Origin binding: the token is tied to the exact origin it was issued for; the request’s
Originmust match. A token lifted into another site is useless. - Scope: the token only grants the operations the bucket allows.
- Single-use nonce: each write carries a fresh
X-Recon-Nonce; replaying a captured write is rejected. - Rate limit per app + IP, and optionally a Turnstile human-proof.
Because the token is short-lived, origin-bound, and scope-limited, the worst case for a leaked page token is “make the writes the page could already make, from the page’s own origin, until it expires”, not data theft.
Collected data can’t be exfiltrated from the page
Section titled “Collected data can’t be exfiltrated from the page”Collection buckets are insert-only from the browser: the page can append, but there is no read path exposed to it. Reading submissions requires a separate, HMAC-sealed owner key (used by the CLI), never present in page code. So even with full knowledge of your page source and a live token, an attacker cannot list what others submitted. A bucket declared public is the deliberate exception: only the fields explicitly marked public are ever projected back, never a personal one. KV, by contrast, is explicitly public; use it only for non-secret shared state.
Site passwords
Section titled “Site passwords”The optional site password is hashed with PBKDF2-HMAC-SHA256 (600,000 iterations, per-site salt); the plaintext is never stored and the hash is never returned. Unlock state is a signed, host-only cookie bound to the site’s internal id (HttpOnly, Secure, SameSite=Lax, 7-day TTL): unlocking one site never unlocks another, and reassigning an address invalidates old cookies. The gate covers the page and every asset, with unlock attempts rate-limited. It’s a soft barrier for private sharing, not a vault for sensitive data.
Internal pages (organization SSO)
Section titled “Internal pages (organization SSO)”A site’s audience can be set to organization members instead of public or password. A visitor is proven on the site’s own origin, never the dashboard session (a published site can’t borrow a control-plane session). They’re bounced through a Supabase-backed sign-in; the server verifies the JWT, checks that the person is a member of the site’s owning organization, and only then sets a signed, host-only cookie (__Host-, HttpOnly + Secure + SameSite=Lax, short TTL) scoped to that one site. Membership is checked at sign-in, fail-closed (if the control plane is unreachable, no cookie is issued). The identity carried is the person’s account id, never the sign-in method, so company SSO (Okta/SAML via the organization’s domain) slots in without changing the flow. The audience governs the whole surface: the in-page data token isn’t minted for a non-member either, and gated responses are private, no-store.
Private per-member journals (owned)
Section titled “Private per-member journals (owned)”On an organization page — and only there, because it’s the only place a visitor has a proven identity — a bucket can be declared owned: a private journal where each member writes and reads back only their own entries. Three properties are enforced in the authorization kernel, not by convention:
- Member ≠ member. A self-read (
?view=self) returns only the caller’s rows, filtered server-side by the member’s identity taken from the verified session, never the request body. An absent identity is refused outright, never a fallback to “all rows”. Two members never see each other’s entries, and a member can erase (DELETE) only their own. - The owner can’t read the content either.
ownedis the one visibility the site owner is not granted read access to — not in the dashboard, the CLI, the AI tools, or the owner API (it’s denied ahead of the usual owner blanket grant). The owner keeps lifecycle control (counts, a mandatory retention window that auto-deletes) but never sees the entries, and never runs a side effect off them (on_insertis rejected onowned). “Private, even from the organizer” is a guarantee, not a UI choice. The member exercises access (read back) and erasure (delete their own) directly. - Only an app-scoped pseudonym ever reaches the page — never the global identity. The member is keyed by
HMAC(app secret, app id + account id), derived server-side. That pseudonym is what’s stamped on records and carried in the token (the token is signed, not encrypted, so a page can read it); the global account id never leaves the server, and the pseudonym is useless outside this one site — two organizations can’t correlate the same person. On a public or password page there is no identity to attach at all, so the token carries noownedscope and the bucket is simply unreachable — no site ever learns who is browsing it outside its own organization.
Custom domains & TLS
Section titled “Custom domains & TLS”A custom domain is only routed once verified (a live CNAME lookup proves you control the zone and pointed it at us). HTTPS certificates are issued on-demand only for verified domains, which also stops anyone from burning certificate rate limits by pointing arbitrary names at the platform.
Serving hardening
Section titled “Serving hardening”Content-Typeis derived server-side from the file extension, never sniffed from content; unknown types download instead of executing.X-Content-Type-Options: nosniffand a baselineContent-Security-Policyare set on served files.- The service sits behind a trusted proxy; it ignores forgeable
X-Forwarded-Forunless a trusted-proxy count is configured, and can require a shared proxy header so the origin can’t be hit directly. - Timeouts guard against slow-loris; a
SIGHUPreload enables an instant kill-switch without downtime.
Privacy & governance
Section titled “Privacy & governance”- A bucket marked as holding personal data cannot be saved without a stated purpose, enforced on the server, not just the dashboard.
- Retention windows auto-delete old records on a schedule; erasure removes every record tied to a value across all buckets (see Reading & erasing collected data).
- Sensitive actions are recorded in an audit log; the actor is stamped from the verified identity, never from the client.