NubleStation

This page describes Vault’s access-control model: how files become owned by a user, how owners share them, and how the platform decides — server-side — who is allowed to read, edit, or delete a file. The model is Cognito + S3 in spirit: Identity supplies who you are, Vault enforces what you can touch.

Shipped

This model is live (ADR 016). The Bucket app is built on it end-to-end — the snippets below are taken from its source.

The keystone: how the system knows who you are

The most important property — and the strongest security guarantee — is that the app frontend never decides who you are. The platform does, server-side, from something the app cannot touch.

  1. You sign in once at identity.{org}.local. Identity sets a session cookie on .{org}.local that is HttpOnly — JavaScript cannot read it, so app code can never read, steal, or forge it.
  2. Every request your browser makes to api.{org}.local carries that cookie automatically — you don’t attach it, the browser does.
  3. The Gateway takes the cookie, validates it against platform.sessions (real? not expired?), and resolves it to your user_id.
  4. It forwards the request to Vault with your user_id in an HMAC-signed internal header. Vault trusts it only because it’s signed with the Gateway’s secret — a compromised app container cannot forge it.
  5. Vault stamps that user_id as the file’s owner, and uses it for every later access check.
Browser ──cookie (HttpOnly, .{org}.local)──▶ Gateway
                                              │ validate session → resolve user_id
                                              │ sign internal headers (HMAC)

                                            Vault  ── owner / access checks keyed on user_id

In one line: identity is never asserted by the frontend — it’s derived by the Gateway from an unforgeable cookie and signed inward, so a malicious app frontend cannot impersonate another user, because it never handles identity at all.

Two kinds of app

You choose your app’s model simply by whether you wire in Identity:

App typeLogin?Default visibilityAvailable
Communalnoapp-shared (everyone using the app shares everything)+ public
Personal (Drive-like)yes — via Identityprivate+ share per-user · + public

There is no halfway: “private to me” is meaningless if the system doesn’t know who “me” is, so private requires Identity. The moment you want per-user files, you’re a Personal app.

Visibility: every file/folder has exactly one

In a Personal app, each file and each collection (folder) has a single visibility setting:

VisibilityWho can see itSet by
Private (default)the owner onlynothing to do — it’s the default
Sharedspecific people you name, each as viewer or editorthe owner, per user
Publicanyone — even logged-out — via a stable URLowner toggles is_public

The crucial distinction: public ≠ “everyone in the app.” Public means no authentication at all — a public file is fetchable by anyone on the LAN who has the URL. That’s why public is its own tool, separate from sharing: it lets you publish something to the world without inviting strangers into your user database.

Roles

Sharing is per individual user (no group/role grants — kept deliberately simple). Each grant carries one role:

RoleCanHow you get it
ownereverything + share + make public + transferyou uploaded it
editorread, download, upload/overwrite, deletean owner shared it with you
viewerread + download onlyan owner shared it with you
app adminlist + delete/transfer for housekeeping — never reads private contentsimplicit (Console admin)
publicread + downloadthe file is is_public

A collection editor grant is what enables “others can upload into my folder.”

Two gates, by design

Access is two independent layers that stack — both must pass:

  1. App access (Layer 1 — Identity): are you allowed into this app at all? Default-deny, admin-granted. This is the gate every NubleStation app already enforces.
  2. File access (Layer 2 — Vault): within the app, can you see this file? Owner + shares + public.

A consequence worth saying out loud: you can only share a file with someone who is already a member of the app. Creating an account isn’t enough — an admin must first grant them app access (Layer 1); only then do they appear in your share picker (Layer 2). Admins control who’s in the app; owners control who sees their files.

How Vault resolves a request

For any action, with the principal being the cookie-derived user:

public + read              → allow (even anonymous, via the public URL)
principal == owner          → allow all
grant on the file OR its collection → that role's capabilities
app admin                   → list / delete / transfer only — never read contents
otherwise                   → deny

Listing

So a user’s “My Drive” view never fills with other people’s public files, listing is explicit about scope:

CallReturns
listMine(collection?)files you own
listSharedWithMe()files explicitly shared with you (each annotated with your role)
listPublic(collection?)the separate public space
list(collection?)everything you can access — own ∪ shared ∪ public ∪ communal (app-admins see all)

Bucket uses the first three to back its My Files / Shared with me / Public tabs. list() is the catch-all for apps that don’t need the split.

Defaults (locked)

  • New files and collections are private to their owner.
  • Sharing is per individual user, with role viewer or editor.
  • Public is anonymous — no login required to read a public file.
  • Sharing requires the recipient to already have app access.

SDK & example

The @nublestation/vault SDK exposes listMine / listSharedWithMe / listPublic / list, share / unshare / listGrants, plus the existing upload / download / setPublic / delete. @nublestation/identity exposes listAppUsers() to populate a share picker.

The SDK does not take any user argument — identity comes from the cookie. The only setup difference from a communal app is that the Vault client sends the session cookie automatically (credentials: "include", built in).

Loading the three views

This is the whole of Bucket’s view loader — the view toggles which scoped call runs:

// apps/bucket — useVaultStore.ts
import { createVaultClient } from "@nublestation/vault";

const vault = createVaultClient({ url: NUBLE_URL, apiKey: NUBLE_KEY });

async function load(view: "mine" | "shared" | "public") {
  if (view === "mine")   return vault.listMine();          // files I own
  if (view === "shared") return vault.listSharedWithMe();  // shared with me (+ role)
  return vault.listPublic();                               // public space
}

Sharing a file

Owners share with one user at a time, as viewer or editor:

// Who can I share with? (everyone with access to this app, minus me)
const people = await identity.listAppUsers();

// Grant — filename = the file, or null for a whole collection
await vault.share("reports", "q1.pdf", granteeUserId, "editor");

// See and revoke current grants on a resource I own
const grants = await vault.listGrants("reports", "q1.pdf");
await vault.unshare("reports", "q1.pdf", granteeUserId);

Uploading & publishing

Upload is always private to the uploader; setPublic opts a file into the anonymous public URL:

await vault.upload("reports", "q1.pdf", bytes);  // private, owner = me
await vault.setPublic("reports", "q1.pdf", true); // now world-readable

Enforcement is server-side

The SDK is a thin client — every rule above is enforced in Vault, not the browser. A viewer who calls delete(), or anyone trying to read a private file they weren’t granted, gets a 403 regardless of what the frontend allows. The UI hides those buttons (via the per-file role field) purely for ergonomics.

See also