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.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#
# 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-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.
// 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:
// 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:
// 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)#
'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.
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.