Security Best Practices
How to keep Abrova Storage credentials and files secure — never expose the server secret, server-only buckets and rules, a signed Abrova user token on every file request, short-lived signed URLs, file type, size and executable checks, safe serving, environment variables, separate projects for development and production.
Keep your files and credentials secure with these AbrovaStorage best practices.
Never Expose Server Secret#
The serverSecret must never be included in client-side code, mobile apps, or frontend applications. Requests with it skip your access rules and can manage every bucket in the project.
Don't do this
// In React/Vue/Angular component
const storage = new AbrovaStorage({
apiKey: 'ab_live_xxx',
serverSecret: 'ab_srv_xxx', // NEVER!
});Do this
// Call your backend API, which holds the server secret
const { uploadUrl } = await fetch(
'/api/get-upload-url',
{ method: 'POST', body: JSON.stringify({ filename, contentType }) }
).then(r => r.json());Buckets and Rules Are Server-Only#
The API key ships inside your app, so treat it as public. For every project:
- creating, updating and deleting buckets, creating, updating and deleting access rules, applying a rule template, and making signed download and upload links need the server secret; the API key alone (with or without a user token) gets 403 with code
server_secret_required; - uploading, reading, listing and deleting files (including
deleteMany) and image transforms work with the API key plus a signed user token, and follow your access rules.
There is no setting to change this. Do bucket and rule work from your server (Node.js SDK or REST with the server secret) or in the Console.
Write Access Rules#
A bucket with no access rules lets any signed-in user of your app read, upload, delete and list every file in it. Add rules to every bucket an app can reach — see Bucket Policies. For a bucket only your server should touch, apply the closed template: apps are denied everything, and your server (server secret) still works.
Sign users in (one token for every Abrova product)#
Every file request from an app needs a signed Abrova user token, and rules that use {userId} or authenticated see the user in it. A plain user id from the app is never trusted.
Use Storage SDK 0.2.1 or newer in every app. An app that doesn't send a user token can't read or write files.
| Credential | Where it lives | What it proves |
|---|---|---|
| API key (ab_live_…) | Inside your app. Public. | Which project the request belongs to. |
| Server secret (ab_srv_…) | Your server only. Never in an app. | That a request comes from your backend. |
| User token | Your app, one per signed-in user. | Which user is acting. This is the user access rules see. |
Your backend mints the token with the server secret:
// Node.js SDK
const { token, expiresAt } = await storage.createUserToken(session.userId); // optional 2nd arg: lifetime in seconds# Any backend
curl -X POST https://auth.abrova.ir/users/token \
-H "x-api-key: $ABROVA_API_KEY" \
-H "x-server-secret: $ABROVA_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "u_4821", "expiresIn": 3600}'
# → { "token": "eyJhbGciOi…", "userId": "u_4821", "expiresIn": 3600, "expiresAt": "…" }It is the same token Abrova Sync, Chat, Push and A/B Testing accept; Abrova Auth's sign-in token is this same token too. The app hands it to the SDK through its token provider, and the SDK sends it as x-user-token and refreshes it when it expires — see the platform pages.
A token is valid until it expires, so store it with the user's session (Keychain, Keystore or secure storage) and reuse it across app launches. Storage has no revocation, so keep the lifetime short if a signed-out user must lose access quickly.
Without a token (no one signed in), the SDKs don't send file requests: they wait until your app signs a user in (isAwaitingUserToken). A request without a token gets 401 token_required; a bad or expired token gets 401 token_invalid / token_expired.
Use Short-Lived Signed URLs#
Set appropriate expiration times for signed URLs based on your use case:
- Upload URLs — 5 minutes by default, at most 15 minutes (a longer
expiresInis cut to 15 minutes). - Download URLs (normal) — 1 hour.
- Download URLs (sensitive) — 5-15 minutes.
- Maximum for download URLs — 24 hours.
Validate File Types and Sizes#
// Server-side validation before generating upload URL
app.post('/api/upload-url', async (req, res) => {
const { filename, contentType, size } = req.body;
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!allowedTypes.includes(contentType)) {
return res.status(400).json({ error: 'File type not allowed' });
}
// Validate file size (10MB max)
if (size > 10 * 1024 * 1024) {
return res.status(400).json({ error: 'File too large' });
}
// Validate filename (prevent path traversal)
if (filename.includes('..') || filename.includes('/')) {
return res.status(400).json({ error: 'Invalid filename' });
}
// Signed upload URL: the bucket's allowed types apply to contentType
const { url } = await storage.createSignedUploadUrl(
'user-uploads',
`uploads/${Date.now()}-${sanitize(filename)}`,
{ contentType, expiresIn: 300 }
);
res.json({ uploadUrl: url });
});A file uploaded through a signed link is registered when it is first read or listed. Abrova Storage then checks the bucket's size limit and allowed types, and your access rules, again against the real file; a file that fails is deleted. Checking type and size on your server first still gives your users a clear answer before they upload.
Let the bucket check too
- Allowed types and size. Set them in Console › Storage › your bucket › Settings. The type is read from the file's content, not only from its name or declared type, and a refused upload gets 400
type_not_allowedorfile_too_large. - Block executable files. Block executable files is on for new buckets; turn it on in bucket Settings for older buckets that take user uploads. Executable files (by name or content, also inside ZIP files) are then refused with 400
executable_blocked.
Serve Files Safely#
Files are served with X-Content-Type-Options: nosniff and a sandbox content security policy, so an uploaded HTML or SVG file can't run scripts on an Abrova address. From a public link, images (except SVG), video, audio, PDF and plain text open in the browser; every other type, including HTML and SVG, is sent as a download.
Environment Variables#
# .env.local (never commit to git!) ABROVA_STORAGE_API_KEY=ab_live_xxxxxxxxxxxx ABROVA_STORAGE_SERVER_SECRET=ab_srv_xxxxxxxxxxxx # Add to .gitignore .env .env.local .env.*.local
Use a separate project for development, so its data and usage stay apart from production. Each project has its own ab_live_xxx key.