NubleStation

@nublestation/vault is the file storage client for NubleStation apps. Upload documents, images, and any binary data to your organization’s private storage, with per-file public/private access control.

Installation

npm install @nublestation/vault
# or
pnpm add @nublestation/vault

Creating a client

import { createVaultClient } from "@nublestation/vault";

const vault = createVaultClient({
  url:    "http://api.clinic.local",   // your NubleStation gateway
  apiKey: "nbl_<key_id>.<secret>",     // API key from the Console
});

All methods are scoped to the app that issued the API key. One app cannot read or write another app’s files.


Collections

Files are organised into collections — namespaces you define. A collection is just a string in the path; no setup required. Think of it as a folder.

patient-records / john-doe-xray.jpg
invoices        / 2026-01.pdf
avatars         / user-42.png
↑                 ↑
collection        filename

Methods

vault.upload(collection, filename, data)

Upload a file. Throws VaultError(409) if a file with the same name already exists in the collection — delete it first or use a different name.

// From a browser file input
const file = fileInput.files[0];
const result = await vault.upload("patient-records", file.name, file);

// From a Buffer or Uint8Array (Node.js)
import { readFileSync } from "node:fs";
const bytes = readFileSync("./report.pdf");
const result = await vault.upload("reports", "q1.pdf", bytes);

Parameters:

ParameterTypeDescription
collectionstringCollection name (letters, numbers, hyphens, underscores)
filenamestringFilename including extension
dataBlob | Uint8Array | ArrayBufferFile contents

Returns: Promise<FileResult>

Error codes:

CodeStatusMeaning
file_already_exists409File at this path already exists
file_too_large413Exceeds the app’s max file size (default 50 MB)
extension_not_allowed415Extension blocked by the app’s allowed list
invalid_content_type400Body was not multipart/form-data

vault.list(collection?)

List every file the caller can access — own ∪ shared ∪ public ∪ communal (app-admins see all). Omit collection to span all collections.

// Everything I can see
const all = await vault.list();

// Only files in a collection
const records = await vault.list("patient-records");

// Show filenames
for (const file of records) {
  console.log(`${file.collection}/${file.filename} — ${file.sizeBytes} bytes`);
}

Returns: Promise<FileResult[]>


Ownership & sharing (ADR 016)

When your app wires in Identity, every file is owned by the user who uploaded it and is private by default. These methods give the Drive-style views and per-user sharing. They require a session cookie (sent automatically) — see Vault — Access Control for the full model.

// Scoped listings
const mine   = await vault.listMine();          // files I own
const shared = await vault.listSharedWithMe();  // shared with me — each FileResult.role is "viewer"|"editor"
const open   = await vault.listPublic();        // the public space

// Share a file (or a whole collection when filename is null) with one user
await vault.share("reports", "q1.pdf", granteeUserId, "editor"); // "viewer" | "editor"
await vault.unshare("reports", "q1.pdf", granteeUserId);

// Who is a resource I own shared with?
const grants = await vault.listGrants("reports", "q1.pdf");
// → [{ granteeUserId, granteeEmail, granteeName, collection, filename, role, createdAt }]

Get the list of users you can share with from @nublestation/identity:

import { createIdentityClient } from "@nublestation/identity";
const identity = createIdentityClient({ url, identityUrl, app: "bucket" });

const people = await identity.listAppUsers(); // everyone with access to this app, minus you

All of this is enforced server-side. A viewer calling delete(), or reading a private file they weren’t granted, gets 403 no matter what the client does.


vault.download(collection, filename)

Download a file and return its raw bytes as an ArrayBuffer.

const bytes = await vault.download("patient-records", "john-doe-xray.jpg");

// Browser — display an image
const url = URL.createObjectURL(new Blob([bytes], { type: "image/jpeg" }));
document.getElementById("img").src = url;

// Node.js — write to disk
import { writeFileSync } from "node:fs";
writeFileSync("./xray.jpg", Buffer.from(bytes));

Returns: Promise<ArrayBuffer>

Error codes:

CodeStatusMeaning
not_found404File does not exist

vault.setPublic(collection, filename, isPublic)

Make a file public (accessible without an API key) or private.

// Make public
const file = await vault.setPublic("avatars", "user-42.png", true);
// file.isPublic === true

// Construct the public URL manually
const publicUrl = `http://api.clinic.local/vault/${appSlug}/${file.collection}/${file.filename}`;

// Make private again
await vault.setPublic("avatars", "user-42.png", false);

Public files are served at:

http://api.{org}.local/vault/{app_slug}/{collection}/{filename}

No API key required to access a public file. The URL is stable until the file is deleted or made private.

Returns: Promise<FileResult>


vault.delete(collection, filename)

Permanently delete a file. Both the bytes on disk and the metadata in the database are removed. This cannot be undone.

await vault.delete("patient-records", "old-scan.jpg");

Returns: Promise<void>

Error codes:

CodeStatusMeaning
not_found404File does not exist

The FileResult type

All methods that return file metadata use this shape:

interface FileResult {
  id:         string;       // UUID assigned by the platform
  ownerId:    string | null; // Identity user id of the owner (null = communal)
  collection: string;       // collection name
  filename:   string;       // filename as uploaded
  mimeType:   string | null; // detected from file bytes, not the client header
  sizeBytes:  number | null; // file size in bytes
  isPublic:   boolean;      // whether the file is publicly accessible
  createdAt:  string;       // ISO 8601 timestamp
  role?:      "owner" | "editor" | "viewer" | "public"; // your access, on scoped lists
}

Error handling

All methods throw VaultError on non-2xx responses.

import { VaultError } from "@nublestation/vault";

try {
  await vault.upload("docs", "report.pdf", bytes);
} catch (err) {
  if (err instanceof VaultError) {
    console.error(err.status, err.code);
    // 409 file_already_exists
  }
}
class VaultError extends Error {
  status: number; // HTTP status code
  code:   string; // machine-readable error code from the server
}

Complete example

import { createVaultClient, VaultError } from "@nublestation/vault";

const vault = createVaultClient({
  url:    "http://api.clinic.local",
  apiKey: process.env.NUBLE_API_KEY!,
});

// Upload a patient record
async function uploadRecord(patientId: string, file: File) {
  try {
    const result = await vault.upload("records", `${patientId}-${file.name}`, file);
    console.log("Uploaded:", result.id, result.sizeBytes, "bytes");
    return result;
  } catch (err) {
    if (err instanceof VaultError && err.status === 409) {
      console.warn("File already exists — deleting old version");
      await vault.delete("records", `${patientId}-${file.name}`);
      return vault.upload("records", `${patientId}-${file.name}`, file);
    }
    throw err;
  }
}

// List all records for a patient
async function getRecords(patientId: string) {
  const all = await vault.list("records");
  return all.filter(f => f.filename.startsWith(patientId));
}

// Make a report publicly shareable
async function shareReport(filename: string): Promise<string> {
  const file = await vault.setPublic("reports", filename, true);
  return `http://api.clinic.local/vault/tasks/reports/${file.filename}`;
}

File size and type limits

Limits are configured per app by the organization admin in the Console → App → Vault Settings.

SettingDefaultDescription
Max file size50 MBPer-file upload limit
Allowed extensions(all)If set, only listed extensions are accepted (e.g. pdf, jpg, png)

Exceeding the size limit returns VaultError(413, "file_too_large"). Uploading a blocked extension returns VaultError(415, "extension_not_allowed").


Path rules

Collection names and filenames must:

  • Start with a letter or number
  • Contain only letters, numbers, hyphens (-), underscores (_), or dots (.)
  • Not contain .. or /

Invalid paths return VaultError(400, "invalid_collection") or VaultError(400, "invalid_filename").