Web SDK
Browser SDK for Abrova Storage — works with React, Vue, Svelte, vanilla JS. Sign users in with an Abrova user token (tokenProvider, Abrova Auth or your server), upload with progress, list, download, delete, and show images with header-free public links via @abrova/storage-web.
Client-side SDK for browser applications. Works with any framework (React, Vue, Svelte, vanilla JS). TypeScript support included. This page describes @abrova/storage-web 0.2.1.
Installation#
# 1. .npmrc in your project root: @abrova packages come from Abrova Center @abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
# 2. Install npm install @abrova/storage-web # or yarn add @abrova/storage-web
Initialization#
import { AbrovaStorage } from '@abrova/storage-web';
const storage = new AbrovaStorage({
apiKey: 'ab_live_xxxxxxxxxxxx',
tokenProvider: () => auth.getToken(), // the signed-in user's Abrova user token, see step 3
});The Web SDK is for client-side use only. Never put a server secret in browser code. Managing buckets, access rules and signed links is for your server (Node.js SDK); from an app it is refused with server_secret_required (403).
Sign users in (one token for every Abrova product)#
Every file request from an app needs a signed Abrova user token, so your access rules ({userId}, authenticated) see a user nobody can fake. The SDK sends it as x-user-token. It is the same token Abrova Sync, Chat, Push and A/B Testing accept.
With Abrova Auth (@abrova/auth-web), its sign-in token is the Abrova user token. No server needed:
import { createAbrovaAuth } from '@abrova/auth-web';
import { AbrovaStorage } from '@abrova/storage-web';
const auth = createAbrovaAuth({ apiKey: 'ab_live_xxxxxxxxxxxx' });
const storage = new AbrovaStorage({
apiKey: 'ab_live_xxxxxxxxxxxx',
tokenProvider: () => auth.getToken(), // null while signed out: file calls wait
});
// After a sign-in, sign-out or switch of user, ask the provider again
auth.onAuthStateChanged(() => {
storage.refreshUserToken();
});With your own users, your server mints the token with the server secret (createUserToken(userId) in the Node.js SDK, or POST https://auth.abrova.ir/users/token), and the provider fetches it:
const storage = new AbrovaStorage({
apiKey: 'ab_live_xxxxxxxxxxxx',
tokenProvider: async () => {
const res = await fetch('/abrova-token', { method: 'POST' });
return res.ok ? (await res.json()).token : null; // null = no one signed in
},
});Call await storage.refreshUserToken() when the user signs in or out, or pass a token you already hold with await storage.setUserToken(token) (null clears it).
The SDK asks the provider before the first file request, keeps the token, replaces it shortly before it expires, and on token_expired / token_invalid asks once more and retries the request once. Concurrent requests share one provider call.
File calls wait for a signed-in user
Without a token, file calls (upload, listFiles, getFile, getFileByPath, download, delete, deleteByPath, deleteMany) are not sent and do not fail: they wait until your app signs a user in. This is by design: show your sign-in screen meanwhile.
const stop = storage.onAwaitingUserToken(() => showSignIn()); // returns a function that removes the callback if (storage.isAwaitingUserToken) showSignIn();
After sign-in, refreshUserToken() or setUserToken(token) lets every waiting call go on, in order. Bucket calls (listBuckets, getBucket, getBucketByName) and getPublicUrl never wait.
Signed user tokens are always required for file calls from the browser. There is no setting to turn this off.
Don't wait on file calls before sign-in. A file call made while no one is signed in finishes only after sign-in. Don't wait for it in code the user is waiting on (a button handler, a startup check): start it in the background, or after sign-in.
Leave the timeout unset. By default a call waits until sign-in, which is right for background work. Set userTokenWaitTimeout only when a screen waits for the answer and can send the user to sign-in:
const storage = new AbrovaStorage({
apiKey: 'ab_live_xxxxxxxxxxxx',
tokenProvider: () => auth.getToken(),
userTokenWaitTimeout: 30000, // ms; then fails with token_required
});A call that waited that long fails with token_required (401). The timeout option is a different setting: it limits each network request (default 30 seconds) and never covers the wait for a user.
With Abrova Auth, turn on the sign-in method you use (for example Anonymous) in Abrova Auth › Sign-in methods. If it is off, sign-in fails with METHOD_DISABLED, no one is signed in, and file calls wait without an error. While developing, log from onAwaitingUserToken (console.debug('Storage: waiting for a user')) so a silent wait is easy to spot.
Upgrading from 0.2.0: userId / setUserId() are removed. Use tokenProvider.
Upload#
// Upload from a File input, with progress
const input = document.querySelector<HTMLInputElement>('#file-input')!;
const selected = input.files![0];
const bar = document.querySelector('progress')!;
const file = await storage.upload('my-bucket', `uploads/${selected.name}`, selected, {
metadata: { source: 'web-app' },
onProgress: ({ bytesSent, totalBytes }) => {
bar.max = totalBytes;
bar.value = bytesSent;
},
});
console.log('Uploaded:', file.id);
// Upload from string or Uint8Array
await storage.upload('my-bucket', 'notes/readme.txt', 'Hello World!');
await storage.upload('my-bucket', 'images/photo.png', bytes, { contentType: 'image/png' });onProgress is called at most every 50 ms or every 1%, and always once at the end with bytesSent === totalBytes. totalBytes is the whole request, so it is a little larger than the file.
Without contentType, the SDK picks the type from the path's extension (.md → text/markdown, .heic → image/heic, …); contentTypeForPath(path) returns the type it would send. Paths are encoded for you (spaces, #, ? and non-Latin characters are fine). A new path can't contain ?, #, % or a . / .. part: it is refused with invalid_path before anything is sent.
List, Download & Delete#
All of these follow the bucket's access rules.
// List files with pagination ({ files, nextCursor }; nextCursor is null on the last page)
const result = await storage.listFiles('my-bucket', { prefix: 'images/', limit: 50 });
if (result.nextCursor) {
const next = await storage.listFiles('my-bucket', { cursor: result.nextCursor });
}
// Download file as ArrayBuffer
const data = await storage.download(fileId);
// Delete
await storage.delete(fileId);
await storage.deleteByPath('my-bucket', 'uploads/old-file.txt');
const { deleted } = await storage.deleteMany(['file1', 'file2', 'file3']);A StorageFile has id, bucketId, path, fileName, mimeType, size, checksum, metadata, uploadedBy, uploadedAt, createdAt and updatedAt (dates are ISO 8601 strings). A Bucket has id, name, description, visibility, allowedMimeTypes, maxFileSize, blockExecutables, createdAt and updatedAt.
Showing Files & Image Transforms#
For a file in a public bucket, getPublicUrl returns a link that needs no key, token or header, so it goes straight into <img src>, srcset or CSS. Pass image options to get a resized or converted image:
import { publicImageUrl } from '@abrova/storage-web';
// A 600 px wide WebP, straight into <img>
img.src = await storage.getPublicUrl(fileId, { width: 600, format: 'webp' });
// From a public link you saved earlier: same URL, no request
const at = (width: number) => publicImageUrl(savedLink, { width, format: 'webp' });
img.srcset = `${at(600)} 600w, ${at(1200)} 1200w`;- The same options always give the same URL, and each size is cached: repeated views don't count as a new transform.
- Use a few fixed sizes (for example 200, 600 and 1200 wide). One image keeps at most 50 variants; a new one beyond that answers
too_many_variants(429). - When the month's 500 free transforms are used up and your wallet cannot pay for more, an image URL with options answers
plan_feature_locked(403). The plain public link always works. - Options are checked before the URL is built (
invalid_transform):formatjpeg/png/webp/avif,fitcover/contain/fill/inside/outside,width/heightup to 4096,quality1-100,blur0-20,sharpen0-10,rotatein degrees.
A private bucket has no public link (getPublicUrl fails with 403). getUrl(), getDownloadUrl() and getTransformUrl() return API addresses that need the x-api-key and x-user-token headers, so a plain <img src> can't load them. Download the bytes instead, or ask your server for a signed link:
const buffer = await storage.download(fileId); img.src = URL.createObjectURL(new Blob([buffer]));
See Image Transforms for transform errors.
Handling errors#
A refused request throws an AbrovaStorageException with the server's message, the HTTP statusCode and a code. Compare code with the constants in StorageErrorCode:
import { AbrovaStorageException, StorageErrorCode } from '@abrova/storage-web';
try {
await storage.upload('my-bucket', 'notes/today.md', '# Today');
} catch (e) {
if (!(e instanceof AbrovaStorageException)) throw e;
if (e.code === StorageErrorCode.TypeNotAllowed) console.warn(e.message);
}| Status | code | When |
|---|---|---|
| 400 | type_not_allowed, file_too_large, executable_blocked | The bucket's settings refuse the upload |
| 400 | invalid_path, invalid_transform | A path or image option isn't allowed (nothing is sent) |
| 409 | path_taken | A file already exists at this path |
| 401 | token_required | No signed-in user within userTokenWaitTimeout |
| 401 | token_expired, token_invalid | The user token is expired or not valid |
| 403 | access_denied | The bucket's access rules refused it |
| 403 | server_secret_required | A server-only action (buckets, rules, signed links) |
The SDK never follows a redirect.