Appearance
hailer.file
Read, upload and remove files in Hailer's own file storage, manage file tags, and restrict files to chosen readers. (For calling external services, see http.)
| Method | Host op | Returns |
|---|---|---|
get(fileId) | file.get | Promise<FileResponse> |
upload(file, filename, options?) | file.upload | Promise<string> (file id) |
addTags(fileId, tags) | file.addTags | Promise<string[]> |
removeTags(fileId, tags) | file.removeTags | Promise<string[]> |
remove(fileIds) | file.remove | Promise<boolean> |
getScope(fileId) | file.getScope | Promise<FileScope> |
setReaders(fileId, readers) | file.setReaders | Promise<FileScope> |
list(options?) | file.list | Promise<FileListResponse> |
setOwner(fileId, userId) | file.setOwner | Promise<boolean> |
get(fileId)
ts
const file = await hailer.file.get(fileId);
// { filename, data (base64), mimeType, tags: string[] }upload(file, filename, options?)
ts
const fileId = await hailer.file.upload(file, 'photo.png', { isPublic: true });
const privateId = await hailer.file.upload(file, 'contract.pdf', { scoped: true });Takes a browser File. The SDK converts it to base64 before sending (binary can't cross the app↔host bridge directly). Resolves with the new file id.
options.isPublicmarks the file publicly accessible.options.scopedscopes the file to the app's workspace (see Scoped files). This is the only way to scope a file. It starts with no readers: only you and workspace admins can open it until you callsetReaders. Cannot be combined withisPublic, and guests cannot upload scoped files (403). The SDK checks the scope after uploading and throws (code 501) if the Hailer version in use stored the file unscoped; that file stays behind unscoped, visible only to you.
addTags(fileId, tags) / removeTags(fileId, tags)
ts
const tags = await hailer.file.addTags(fileId, ['invoice', '2026']);
await hailer.file.removeTags(fileId, ['2026']);Both return the file's resulting tag list.
- addTags — tags are trimmed; each must be 1–64 characters; a file holds at most 50 tags.
- removeTags — matching is case-sensitive and exact; tags that aren't present are ignored.
On a scoped file, only its owner or a workspace admin may change tags. Readers can see them, but get an error when they try to edit.
remove(fileIds)
ts
await hailer.file.remove([fileId]);Permanently deletes up to 100 files scoped to the app's workspace. If any id is not such a file (an unscoped file, or one scoped to another workspace), the whole call is refused (403) and nothing is removed: an app can never delete the user's profile pictures, attachments or other workspaces' files. Of the files it accepts, only those you own are removed; ids of anyone else's files are skipped without an error, workspace admins included (an admin can take a file over with setOwner first). A removed file is gone from every activity and discussion it was attached to.
Scoped files
By default a file is readable by anyone who can see where it is attached — an activity, a discussion, a feed post. A scoped file belongs to a workspace and is readable only by:
- its owner (the uploader), while they are a full member of the workspace,
- admins and owners of that workspace,
- its readers:
network_<workspaceId>(every full member),team_<teamId>(the team's full members) oruser_<userId>(one full member).
Only full members ever match. Guests have no access to scoped files: network_ and team_ skip them, a user_ entry naming a guest is rejected, and guests cannot upload scoped files or use getScope, setReaders, list or setOwner. A member who leaves the workspace or becomes a guest loses access at once, including to scoped files they own; an admin can hand those over with setOwner.
Things to know before scoping a file:
- Scope at upload.
upload(..., { scoped: true })is the only way to scope a file. Existing unscoped files cannot be scoped from an app. - Scoping is irreversible. A scoped file cannot become unscoped or move to another workspace.
- Attaching grants nothing. A scoped file attached to an activity or discussion is still denied to people who are not readers; they see the attachment but cannot open it.
- The workspace is always the app's own. The host pins every scope, listing and change to the workspace the app is installed in; files scoped to other workspaces are refused with 403, even when the user administers them. Personal apps cannot use scoped files.
- A scoped file is never public.
Build reader entries with hailer.file.readers rather than by hand:
ts
const { readers } = hailer.file;
const workspace = await hailer.workspace.current();
readers.workspace(workspace._id); // 'network_<id>'
readers.team(teamId); // 'team_<id>'
readers.user(userId); // 'user_<id>'getScope(fileId)
ts
const scope = await hailer.file.getScope(fileId);
// { _id, uid (owner), workspaceId?, readers? } — no workspaceId means the file is unscopedWorks for unscoped files you can read, so it doubles as an "is this file scoped?" check, and for files scoped to the app's workspace. A file scoped to another workspace gives 403; a scoped file you cannot read (including any scoped file, for a guest) gives an error as if it did not exist.
setReaders(fileId, readers)
ts
await hailer.file.setReaders(fileId, [readers.team(salesTeamId), readers.user(auditorId)]);
await hailer.file.setReaders(fileId, []); // back to owner + workspace admins onlyReplaces the reader list wholesale (max 100 entries); revoke by sending the list without the entry. Only for files scoped to the app's workspace: an unscoped file is refused (403), never scoped here. Requires being the owner (while a full member) or a workspace admin. A non-admin may only add teams they belong to; teams already on the list stay.
Every entry must belong to the app's workspace: network_ must be that workspace, team_ a team in it, and user_ a full member (not a guest). Otherwise the call fails with code 191; workspace admins also get the failing entries in details.invalid.
list(options?)
ts
const { files, hasMore, total } = await hailer.file.list({ search: ['invoice', '2026'], limit: 50 });Lists the scoped files of the app's workspace that you can read: your own, those you are a reader of, or all of them if you are a workspace admin. Unscoped files are not listed. Not available to guests.
| Option | Meaning |
|---|---|
search | One keyword or an array of up to 20. A keyword matches a case-insensitive substring of the file name or of any tag. |
matchType | 'all' (default) — every keyword must match; 'any' — at least one. |
skip, limit | Paging; limit 1–100, default 100. |
sortBy, reverse | 'name' or 'created'. |
Each file is { _id, name, uid, workspaceId, type?, length?, created?, updated?, tags? }; readers are never included.
setOwner(fileId, userId)
ts
await hailer.file.setOwner(fileId, newOwnerId);Transfers a file scoped to the app's workspace to another full member of it (not a guest). Workspace admins only. The previous owner loses access unless they are a reader.
Signals
Subscribe with subscribedSignals (see signals):
file.tags.update—{ fileId, tags }when tags change on an unscoped file.file.update—{ fileId }when a file's readers or owner change, or its tags change while it is scoped. The payload deliberately carries no details; re-read withgetScope.file.remove—{ fileIds }when files are removed.
These arrive for files in any workspace the user belongs to, not only the app's. Ignore ids your app does not know.