Abrova
☰ Docs menu · Node.js
Coming soon. Abrova Storage is coming soon. This page describes how it will work; it can’t be used yet.

Node.js SDK

Server-side TypeScript SDK for Abrova Storage on Node.js. Always runs with your server secret. Mint Abrova user tokens (createUserToken), manage buckets and policies, upload with progress, download, list, delete, signed URLs, public image links and transforms via @abrova/storage-node.

Node.js 0.2.1Last updated: 27 Oct 2026

Server-side TypeScript SDK for Node.js. Includes complete type definitions. This page describes @abrova/storage-node 0.2.1.

This is the SDK for your server: it always runs with your server secret. Apps use the Web, React Native or mobile SDKs with a signed user token.

Installation#

bash
# 1. .npmrc in your project root: @abrova packages come from Abrova Center
@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
bash
# 2. Install
npm install @abrova/storage-node
# or
yarn add @abrova/storage-node
# or
pnpm add @abrova/storage-node

Initialization#

typescript
import { AbrovaStorage } from '@abrova/storage-node';

const storage = new AbrovaStorage({
  apiKey: process.env.ABROVA_API_KEY!,             // ab_live_...
  serverSecret: process.env.ABROVA_SERVER_SECRET!, // ab_srv_..., server only, never in an app
});

Optional: timeout limits each network request, in milliseconds (default 30000):

typescript
const storage = new AbrovaStorage({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET!,
  timeout: 60000,
});

serverSecret is required: without it the constructor throws INVALID_ARGUMENT, and it refuses to run in a browser. Calls made with the server secret skip bucket access rules, so check in your own code that the user may do what you do on their behalf.

Tip.

Use environment variables for credentials and never commit them to version control. The SDK keeps the secret in a private field, so it never shows up in console.log(storage) or error reports.

Upgrading from 0.2.0: serverSecret is now required and the userId option is removed.

Sign users in (one token for every Abrova product)#

File requests from your app always need a signed Abrova user token, so access rules ({userId}, authenticated) see a user nobody can fake. Your server mints it and hands it to the app, whose SDK takes it through its tokenProvider option.

typescript
// Express example, behind your own session check
app.post('/abrova-token', async (req, res) => {
  const { token, expiresAt } = await storage.createUserToken(req.session.userId);
  // optional lifetime in seconds: createUserToken(userId, 900)
  res.json({ token, expiresAt });
});

createUserToken(userId, expiresIn?) returns { token, userId, expiresIn, expiresAt }. expiresIn is in seconds: 1 hour by default, 24 hours maximum. It posts to https://auth.abrova.ir/users/token (set the authUrl option to change it).

The token is the same one Abrova Sync, Chat, Push and A/B Testing accept, so this one endpoint serves every Abrova SDK in your app. Apps that sign users in with Abrova Auth need no endpoint: its sign-in token is the same Abrova user token. Both kinds work; keep one source of user ids per app.

Buckets#

typescript
const bucket = await storage.createBucket('user-uploads', {
  visibility: 'private',            // or 'public' for header-free public links
  allowedMimeTypes: ['image/*'],
  maxFileSize: 10 * 1024 * 1024,
});

const buckets = await storage.listBuckets();
const byName = await storage.getBucketByName('user-uploads');

A Bucket has id, name, description, visibility, allowedMimeTypes (null = every type), maxFileSize, blockExecutables, createdAt and updatedAt.

Upload Files#

typescript
import fs from 'fs';

// Upload from Buffer, with progress
const buffer = fs.readFileSync('./image.jpg');
const file = await storage.upload('my-bucket', 'images/photo.jpg', buffer, {
  contentType: 'image/jpeg',
  metadata: { category: 'profile' },
  onProgress: ({ bytesSent, totalBytes }) => {
    console.log(`${Math.round((bytesSent / totalBytes) * 100)}%`);
  },
});

console.log('Uploaded:', file.id);

// Upload from string (content type from the extension: text/markdown)
await storage.upload('documents', 'notes/readme.md', '# Hello');

Without contentType, the SDK picks the type from the path's extension; contentTypeForPath(path) returns the type it would send. Paths are encoded for you. A new path can't contain ?, #, % or a . / .. part: it is refused with invalid_path before anything is sent.

A StorageFile has id, bucketId, path, fileName, mimeType, size, checksum, metadata, uploadedBy, uploadedAt, createdAt and updatedAt (dates are ISO 8601 strings).

Download Files#

typescript
// Download file content as Buffer
const buffer = await storage.download(fileId);

// Save to file
fs.writeFileSync('./downloaded.jpg', buffer);

List Files#

typescript
const result = await storage.listFiles('my-bucket', {
  prefix: 'images/',  // Filter by path prefix
  limit: 50,          // 1-1000, default 100
});

console.log('Files:', result.files);

// Pagination: nextCursor is null on the last page
if (result.nextCursor) {
  const nextPage = await storage.listFiles('my-bucket', {
    prefix: 'images/',
    cursor: result.nextCursor,
  });
}

Delete Files#

typescript
// Delete single file
await storage.delete(fileId);
await storage.deleteByPath('my-bucket', 'uploads/old-file.txt');

// Delete multiple files (up to 1000 ids)
const result = await storage.deleteMany(['file_id_1', 'file_id_2', 'file_id_3']);
console.log(`Deleted ${result.deleted} files`);

Signed URLs#

Signed links need no headers, so they work in <img src>, an image view or a download link. They stay valid until they expire.

Generate Signed Download URL

typescript
// Time-limited download URL for a private file
const signedUrl = await storage.getSignedUrl(fileId, {
  expiresIn: 3600, // seconds; default 3600, max 86400 (24 hours)
});

res.json({ downloadUrl: signedUrl });

Generate Signed Upload URL

typescript
app.post('/api/get-upload-url', async (req, res) => {
  const { filename, contentType } = req.body;

  // Returns { url, uploadId, path, expiresAt, maxSize }
  const { url, expiresAt } = await storage.createSignedUploadUrl(
    'user-uploads',
    `uploads/${Date.now()}-${filename}`,
    {
      contentType,    // must be allowed by the bucket
      expiresIn: 300, // seconds; default 300, at most 900 (15 minutes)
    }
  );

  res.json({ uploadUrl: url, expiresAt });
});

// The app then uploads directly, with the same Content-Type:
// await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file })

An upload link is valid for at most 15 minutes. The bucket's type and size limits apply to the uploaded file.

Image Transformations#

For a file in a public bucket, getPublicUrl returns a link that needs no key or header, ready for the <img src> of the pages, e-mails or API answers your server sends. Pass image options for a resized or converted image:

typescript
import { publicImageUrl } from '@abrova/storage-node';

const url = await storage.getPublicUrl(fileId, { width: 600, format: 'webp' });
html += `<img src="${url}" width="600" alt="">`;

// From a public link you saved earlier: same URL, no request
const thumb = publicImageUrl(savedLink, { width: 200, height: 200, fit: 'cover', format: 'webp' });
  • 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.
  • A file in a private bucket has no public link (403): use getSignedUrl() or download().

getTransformUrl(), getUrl() and getDownloadUrl() build API addresses that need the x-api-key header, so they can't go straight into a browser's <img src>. Fetch them from your server with your headers:

typescript
const thumbnailUrl = storage.getTransformUrl(fileId, {
  width: 800,        // 1-4096 (larger values are sent as 4096)
  height: 600,
  fit: 'contain',    // cover, contain, fill, inside, outside
  format: 'avif',    // jpeg (default), png, webp, avif
  quality: 85,       // 1-100
  blur: 2,           // 0-20
  sharpen: 1,        // 0-10
  rotate: 90,        // degrees
});

Any other value throws invalid_transform before the URL is built. See Image Transforms for transform errors.

Bucket Policies#

typescript
// Create custom policy rules
await storage.createPolicy('my-bucket', [
  {
    name: 'Allow authenticated read',
    action: 'read',
    condition: { authenticated: true },
    effect: 'allow',
    priority: 10,
  },
  {
    name: 'Users can only access their own folder',
    action: '*',
    condition: {
      authenticated: true,
      pathPattern: 'users/{userId}/**',
      matchUserId: true,
    },
    effect: 'allow',
    priority: 20,
  },
  {
    name: 'Limit upload size for images',
    action: 'write',
    condition: {
      authenticated: true,
      allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
      maxSize: 5 * 1024 * 1024, // 5MB
    },
    effect: 'allow',
  },
]);

// Or apply a pre-built template
await storage.applyPolicyTemplate('my-bucket', 'userFolders');

// Available templates:
// - 'publicRead'   - Public read, authenticated write
// - 'private'      - Authenticated only
// - 'userFolders'  - Users can only access users/{userId}/**
// - 'imagesOnly'   - Image uploads up to 5MB by authenticated users
// - 'closed'       - Server only: apps are denied everything, the server secret still works

{userId} and authenticated come from the app's Abrova user token. A rule that isn't valid is refused with invalid_rules.

Complete Example: Next.js API Routes#

typescript
// lib/storage.ts
import { AbrovaStorage } from '@abrova/storage-node';

export const storage = new AbrovaStorage({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET!,
});

// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { storage } from '@/lib/storage';
import { getServerSession } from 'next-auth';

export async function POST(req: NextRequest) {
  const session = await getServerSession();
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { filename, contentType } = await req.json();

  const { url, expiresAt } = await storage.createSignedUploadUrl(
    'user-uploads',
    `users/${session.user.id}/${Date.now()}-${filename}`,
    { contentType, expiresIn: 300 }
  );

  return NextResponse.json({ uploadUrl: url, expiresAt });
}

// app/api/files/[id]/route.ts
export async function DELETE(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const session = await getServerSession();
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  // The server secret skips access rules: check that this user owns the file
  const file = await storage.getFile(params.id);
  if (!file.path.startsWith(`users/${session.user.id}/`)) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  await storage.delete(params.id);
  return NextResponse.json({ success: true });
}

See Next.js Integration for the browser side.

Error Handling#

A refused request throws an AbrovaStorageError with the server's message, the HTTP statusCode and a code. Compare code with the constants in StorageErrorCode:

typescript
import { AbrovaStorageError, StorageErrorCode } from '@abrova/storage-node';

try {
  await storage.upload('bucket', 'file.jpg', buffer);
} catch (error) {
  if (!(error instanceof AbrovaStorageError)) throw error;

  switch (error.code) {
    case StorageErrorCode.FileTooLarge:
    case StorageErrorCode.TypeNotAllowed:
      console.warn(error.message);
      break;
    case StorageErrorCode.PathTaken:
      // a file already exists at this path
      break;
    default:
      console.error(error.statusCode, error.code, error.message);
  }
}
StatuscodeWhen
400type_not_allowed, file_too_large, executable_blockedThe bucket's settings refuse the upload
400invalid_path, invalid_transformA path or image option isn't allowed (nothing is sent)
400invalid_rules, invalid_idsA policy rule or a deleteMany list isn't valid
409path_takenA file already exists at this path
403server_secret_requiredA server-only action without a valid server secret

The SDK never follows a redirect.

Was this page helpful?
Report a problem