HyphaBase

Files

Upload the brief, attach it to the campaign, hand out a link that expires. The bytes never touch our API.

Sarah's spring campaign has a brief — a PDF from the agency. It belongs with the campaign record, it mustn't be reachable by guessing a URL, and it shouldn't crawl through our API server on its way to storage.

Files is the environment's file catalog: each file is a row in the same database as your custom data, and an object in storage under a prefix that is yours alone. Uploads go straight to storage through a short-lived URL; downloads come back through an expiring URL; every upload is virus-scanned before anyone can read it.

Server-side, in this release

Files are reached with a secret (sk_) key. Your backend uploads on a user's behalf and hands them the download URL. End-user access rules are a later release.

Three steps, or one

Upload

The SDK does the dance for you: announce the file, PUT the bytes where the API says, complete.

const brief = await hb.files.upload(pdfBytes, {
  name: "spring-brief.pdf",
  contentType: "application/pdf",
  metadata: { kind: "brief" },
});
// brief.status === "ready", brief.scan_status === "pending"

Doing it by hand — from a mobile app, say — is the same three calls:

POST /api/modules/files/uploads        { "name", "content_type", "size", "metadata"? }
  → 201 { "data": { "id", ... }, "upload": { "url", "method": "PUT", "headers", "expires_at" } }

PUT  <upload.url>              the bytes, with the headers given (no API key)

POST /api/modules/files/{id}/complete  → 200, verifies the object's size against what you declared

The upload URL is bound to the declared size and content type and lives a few minutes. The object key is ours (app/…/env/…/{id}), never your file name.

Wait for the scan

Every upload is scanned for malware off the request. Until the verdict is in, the file can be attached to records but not downloaded.

const scanned = await hb.files.waitUntilScanned(brief.id);
// scan_status: "clean" | "infected" | "failed"

An infected file is moved to quarantine, cannot be downloaded or attached, and fires file.infected. Subscribe to file.ready instead of polling if you'd rather be told.

Attach it to a record

A field of type file holds the id — and is a real foreign key to the catalog.

await hb.data.collections.create({
  slug: "campaigns",
  fields: [
    { name: "name",  type: "string", required: true },
    { name: "brief", type: "file", on_delete: "restrict" },   // or "set_null" (default)
  ],
});

await campaigns.create({ name: "Spring refresh", brief: brief.id });

Only a file that is uploaded, not deleted and not infected can be attached. With restrict, deleting the file answers 409 while records point at it; with set_null, the field clears when the file is finally purged.

const { url, expires_at } = await hb.files.url(brief.id, { expiresIn: 300 });

The URL goes straight to storage, carries the file name for the browser, and dies at expires_at (default 10 minutes, at most an hour). Ask for a fresh one each time someone needs it; there is nothing to revoke.

Delete, and get it back

await hb.files.delete(brief.id);   // gone from lists and URLs; the object is kept
await hb.files.restore(brief.id);  // within 30 days

After 30 days the object is dropped for good. Uploads announced but never completed are dropped after a day.

Attach it to a module record

Custom data fields are for your own collections; for records a module owns — a client, say — attach the file to the record in the location that holds it:

const link = await hb.files.attach(brief.id, {
  targetType: "clients",
  targetId: client.id,
  locationId: location.id,
  label: "contract",
});

const docs = await hb.files.list({ attached_to: `clients:${client.id}`, location_id: location.id });
// each file carries its `attachment` (label, when)

await hb.files.detach(brief.id, link.id);   // the file stays in the catalog

The key must reach the location and the record must exist there; attaching the same file twice returns the existing link. Links survive a soft delete of the file and go with it when it's purged. Today clients is the attachable record type; modules opt in as they are released.

Keys and scopes

files.read lists, reads and mints download URLs; files.upload announces, completes and edits metadata; files.delete deletes and restores. See Who can do what.

Limits

Per plan: the largest single upload (files.max_upload_bytes, 25 MB on sandbox, 250 MB on production) and total storage (1 GB / 50 GB), both checked when an upload is announced. Metadata is at most 4 KB.

Events

file.ready, file.infected, file.deleted, file.restored — see Getting told when things happen.

Beta

Files are in beta: the shapes above are what we intend to keep, and anything that changes before general availability will be announced with a dated API version.

On this page