PublicViews

Security

What leaves your workspace, and what never does.

What we hold and what we drop before the page is built. Written for the person who has to approve this.

What we store

We hold a copy of your records

A published page reads a cache, not Attio. That is how a rush of traffic to your client’s view never touches your Attio rate limit. What we cache is the projection your views need, and nothing else.
AttributeOn our serversIn the viewer’s browser
An attribute you switched onStoredSent to the viewer
An attribute that sorts the view or scopes its rowsStoredNever sent
An attribute your filter testsNot stored. Attio evaluates the filterNever sent
Everything else in your workspaceNever storedNever sent

Exposure

Nothing is visible until you switch it on

A new view publishes nothing. Not a sensible default set, not the first five columns. Zero. The publish screen flags the attributes worth a second look rather than counting them: currency, references to people, email addresses, phone numbers, personal names, and any label that reads like salary, cost, margin, internal or private. Then it asks you to confirm, in those words, before a view goes out with no password, no email gate and no expiry. After it is live, a view whose membership jumps sharply pauses itself and asks you, and a weekly digest tells you what of yours is currently public.

Rendering

Hidden attributes never reach the browser

Only the attributes you made visible are ever serialized to a viewer. Not filtered out in the browser, not collapsed with CSS. It is enforced twice: the repository selects only those keys out of the stored values, and a single projector the read path cannot go around does the serializing. Pagination is where this sort of promise usually leaks, so the cursor is a server-side signed token rather than an encoded sort value. A hidden attribute cannot ride out inside something a reviewer reads as opaque.

Writes

Read-only until you decide otherwise

Every view is read-only until you mark specific attributes editable, per attribute and enforced on our server. By default a viewer’s change is a proposal: it waits in your dashboard beside the value Attio holds now, and we re-read that one attribute at approval rather than writing over something that moved. A view can instead apply edits straight away. Only a viewer with a verified email address can edit one, every change runs the same checks, and the value it replaced is recorded. The target row is resolved through the viewer’s own accessor first, so a row key belonging to a record outside the view returns a 404 byte-identical to the one for a record that does not exist. Write endpoints are rate-limited per session, per view and per tenant.

Access

One way in, and it fails closed

Pick one way in: an unlisted link, a shared password, or an email or domain allowlist. A password and an allowlist can’t both be on, because a password proves someone knows a secret and an allowlist proves who they are. On top of whichever you pick, the link can close on a date or after a number of opens. With an allowlist a viewer types a listed address, and you can make them confirm it by email; showing each viewer only their own rows, or letting their edits apply without approval, turns that confirmation on and keeps it on. A gate never tells a stranger anything: an address that is not on the list gets the same screen as one that is. Viewer sessions and publisher sessions are different cookie namespaces signed with different secrets, and the code that serves published views has no import path to a publisher session, to the database handle, or to Attio. We check that through the whole import graph on every build rather than leaving it to reviewers.

Revocation

Unpublish takes effect on the next request

Published views are never held in a shared cache. No incremental static regeneration, no CDN copy of the HTML, no shared max-age; the rows are cached on our own infrastructure and the view itself is assembled per request. So no request that begins after you pause sees data. The status is read before any row is, and the same transaction signs out every existing viewer session. That costs us a little speed and buys the one property this product cannot do without.

Isolation

One customer cannot reach another

Every application table carries a tenant id and it cannot be null. The application never holds a raw database handle. It holds a repository with the tenant closed over at construction, so no method takes a tenant argument, there is no argument to pass wrong, and an id belonging to another workspace returns not found rather than confirming that it exists. Foreign keys are compound on the row and the tenant together. We do not use Postgres row-level security and would rather say why than quote it as a feature: on our managed database the application role can bypass those policies, so enabling them would produce rules that read as protection and are silently ignored on every query.

Found something? support@publicviews.app

Send enough detail to reproduce it. We will confirm we have it, tell you what we found, and tell you when it is fixed.

Publish your first view