Nova Admin Docs
Guides

Access & sharing

The unified visibility model for published entities — public, workspace, and private tiers plus the per-user sharedWith ACL, and the /p/{type}/{slug} reader URL.

Every publishable entity in Nova — documents, threads, skills, flows, agents, composio-presets, and app artifacts — shares one access model. A single pure function, resolveEntityAccess() (lib/library/access.ts), decides "who may read this", and both the public reader route and the app-artifact API delegate to it so they can never drift apart.

The shareable URL

Published entities are read at:

/p/{type}/{slug}

{type} is a one-letter publishable-type code:

CodeEntity
ddocument
tthread
sskill
fflow
aagent
ccomposio-preset
rartifact

The slug is only a locator — it resolves the record, but access is then decided from the entity's current stored visibility. Revoking access is a visibility change, not a slug change. Document URLs additionally accept ?v=<n> to pin to a previously-published snapshot. All reader pages are marked noindex.

Visibility tiers

Visibility is a strict ladder, most-open first: publicworkspaceprivate. The model is default-deny — any unrecognized tier denies.

TierAnonymous (no session)Authenticated memberRule
public✅ can view✅ can viewReadable by anyone, including logged-out visitors.
workspace✅ can viewAny valid session. Nova is a single-workspace product, so "has a verified session" means "is a workspace member".
privateOwner or shared email onlyReadable only by the owner (uid === ownerId) or an email in sharedWith.

The sharedWith ACL

For private entities, access can be extended to specific people via sharedWith — a list of entries keyed by email:

sharedWith: [{ email: "teammate@example.com", role: "viewer" }]

For the read decision only email matters; emails are compared case-insensitively. The role field (viewer/editor) is irrelevant to read access. ownerId and sharedWith are consulted only for the private tier — they are ignored for public and workspace.

How a request is gated

  1. The reader resolves the slug to an entity and reads its current visibility.
  2. resolveEntityAccess({ visibility, ownerId, sharedWith }, session) returns { allowed }.
  3. If allowed, the per-type renderer produces a read-only public view; otherwise the reader returns not-found / denied.

Because the gate is a single pure function shared by every surface, a new publishable type or a new reader inherits the exact same rules with no extra wiring.

On this page