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

Next.js Integration

Add Abrova Storage to a Next.js project — server-side via @abrova/storage-node for route handlers and server components (uploads, signed URLs, public image links, Abrova user tokens), client-side via @abrova/storage-web for browser components with a tokenProvider and upload progress.

Next.jsSDKs 0.2.1Last updated: 27 Oct 2026

Next.js uses two SDKs: @abrova/storage-node on the server (route handlers, server components) and @abrova/storage-web in the browser (client components). This page describes version 0.2.1 of both.

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 @abrova/storage-web

Server-Side (API Routes)#

The Node.js SDK always needs your server secret and skips access rules, so check who the user is in your route before you act.

typescript
// lib/storage.ts — shared server instance (never import this from a client component)
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 formData = await req.formData();
  const file = formData.get('file') as File;
  const buffer = Buffer.from(await file.arrayBuffer());

  const uploaded = await storage.upload(
    'user-uploads',
    `users/${session.user.id}/${file.name}`,
    buffer,
    { contentType: file.type }
  );

  return NextResponse.json({ fileId: uploaded.id });
}

// app/api/files/[id]/route.ts — signed download URL (no headers needed, usable in <img>)
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  const session = await getServerSession();
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  // Check here that this user may see this file
  const signedUrl = await storage.getSignedUrl(params.id, { expiresIn: 3600 });
  return NextResponse.json({ url: signedUrl });
}

For a file in a public bucket, a server component can render the public image link directly. It needs no headers:

tsx
// components/ProductImage.tsx (a server component)
import { storage } from '@/lib/storage';

export async function ProductImage({ fileId }: { fileId: string }) {
  const src = await storage.getPublicUrl(fileId, { width: 600, format: 'webp' });
  return <img src={src} width={600} alt="" />;
}

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

File requests from the browser always need a signed Abrova user token, so access rules ({userId}, authenticated) see a user nobody can fake. It is the same token Abrova Sync, Chat, Push and A/B Testing accept.

Using Abrova Auth? Its sign-in token is this token: use tokenProvider: () => auth.getToken() and skip this route. Otherwise a route handler mints it with the server secret:

typescript
// app/api/abrova-token/route.ts
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { storage } from '@/lib/storage';

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

While no user is signed in, the Web SDK's file calls wait instead of failing (see Web SDK); they go on once the provider returns a token.

Client-Side (React Components)#

tsx
'use client';
import { useEffect, useState } from 'react';
import { AbrovaStorage } from '@abrova/storage-web';
import { useSession } from 'next-auth/react';

const storage = new AbrovaStorage({
  apiKey: process.env.NEXT_PUBLIC_ABROVA_API_KEY!,
  tokenProvider: async () => {
    const res = await fetch('/api/abrova-token', { method: 'POST' });
    return res.ok ? (await res.json()).token : null; // null = no one signed in: file calls wait
  },
});

export function UploadButton() {
  const { data: session } = useSession();
  const [progress, setProgress] = useState(0);

  // Ask the provider again when the user signs in or out
  useEffect(() => {
    storage.refreshUserToken();
  }, [session?.user?.id]);

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file || !session?.user?.id) return;

    const uploaded = await storage.upload('my-bucket', `users/${session.user.id}/${file.name}`, file, {
      onProgress: ({ bytesSent, totalBytes }) => setProgress(bytesSent / totalBytes),
    });
    console.log('Uploaded:', uploaded.id);
  };

  return (
    <>
      <input type="file" onChange={handleUpload} />
      <progress value={progress} max={1} />
    </>
  );
}

// Private files: getTransformUrl() / getUrl() need headers, so ask your server for signed links
export function ImageGallery({ fileIds }: { fileIds: string[] }) {
  const [urls, setUrls] = useState<Record<string, string>>({});

  useEffect(() => {
    Promise.all(
      fileIds.map(async (id) => {
        const { url } = await fetch(`/api/files/${id}`).then((r) => r.json());
        return [id, url] as const;
      }),
    ).then((pairs) => setUrls(Object.fromEntries(pairs)));
  }, [fileIds]);

  return (
    <div className="grid grid-cols-3 gap-4">
      {fileIds.map((id) => urls[id] && <img key={id} src={urls[id]} alt="" />)}
    </div>
  );
}

For public buckets, use getPublicUrl(fileId, { width, format }) in either SDK, or publicImageUrl(savedLink, { width: 400, format: 'webp' }) to build a size from a public link you saved, with no request. Use a few fixed sizes: one image keeps at most 50 variants (too_many_variants), and when the free monthly transforms are used up and the wallet cannot pay for more, image URLs answer plan_feature_locked. See Image Transforms.

Errors from either SDK carry code and statusCode (AbrovaStorageError on the server, AbrovaStorageException in the browser), with constants in StorageErrorCode.

Environment variables.

Use the NEXT_PUBLIC_ prefix only for the public API key. Never expose the server secret: read ABROVA_SERVER_SECRET only in route handlers and server components.

Was this page helpful?
Report a problem