| title | Storage |
|---|---|
| description | Upload files and images to get public CDN URLs. No S3 buckets or complex configuration required. |
urBackend handles file and image uploads for you. Upload a file and receive a public CDN URL you can use directly in your application.
Base URL: https://api.ub.bitbros.in
Uploads use a presigned URL three-step flow so the binary is sent directly to storage.
Endpoint: POST /api/storage/upload-request
Request body (size in bytes):
{
"filename": "avatar.png",
"contentType": "image/png",
"size": 123456
}Response:
{
"signedUrl": "https://storage.example.com/...",
"token": "optional-provider-token",
"filePath": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png"
}| Field | Description |
|---|---|
signedUrl |
Presigned URL to upload the binary directly to storage |
token |
Provider-specific token (only returned for some external providers) |
filePath |
Opaque storage path (format: projectId/uuid_sanitizedFilename) — save this to confirm or delete the file later |
Send a browser PUT request to signedUrl with the raw file contents and the correct Content-Type header.
const uploadRes = await fetch(signedUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (!uploadRes.ok) {
throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
}Endpoint: POST /api/storage/upload-confirm
Request body:
{
"filePath": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png",
"size": 123456
}Response:
{
"message": "Upload confirmed",
"path": "PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png",
"provider": "internal",
"url": "https://xyz.supabase.co/storage/v1/object/public/dev-files/PROJECT_ID/550e8400-e29b-41d4-a716-446655440000_avatar.png"
}| Field | Description |
|---|---|
message |
Confirmation message |
path |
Normalized storage path — use this to delete the file later |
provider |
Storage backend used (internal or external) |
url |
Public URL for the file. If unavailable, this is null. |
warning |
Optional warning message when a public URL is unavailable |
If the storage provider does not expose a public URL (for example, some external S3/R2 setups), the response returns url: null and includes a warning field explaining that a public URL is not available.
if (!fileInput?.files?.length) {
throw new Error('No file selected');
}
const file = fileInput.files[0];
const requestRes = await fetch('https://api.ub.bitbros.in/api/storage/upload-request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_YOUR_KEY'
},
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size
})
});
if (!requestRes.ok) {
const errorBody = await requestRes.json().catch(() => null);
throw new Error(
`Upload request failed: ${requestRes.status} ${requestRes.statusText}${
errorBody?.error ? ` - ${errorBody.error}` : errorBody?.message ? ` - ${errorBody.message}` : ''
}`
);
}
const { signedUrl, filePath } = await requestRes.json();
const uploadRes = await fetch(signedUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
if (!uploadRes.ok) {
throw new Error(`Upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
}
const confirmRes = await fetch('https://api.ub.bitbros.in/api/storage/upload-confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_YOUR_KEY'
},
body: JSON.stringify({ filePath, size: file.size })
});
if (!confirmRes.ok) {
if (confirmRes.status === 409) {
const retryableError = await confirmRes.json();
throw new Error(
`Upload not ready yet: ${retryableError.error || retryableError.code || 'UPLOAD_NOT_READY'}. Retry the confirm request shortly.`
);
}
const errorBody = await confirmRes.json().catch(() => null);
throw new Error(
`Upload confirm failed: ${confirmRes.status} ${confirmRes.statusText}${
errorBody?.error ? ` - ${errorBody.error}` : errorBody?.message ? ` - ${errorBody.message}` : ''
}`
);
}
const { url, provider, path, warning } = await confirmRes.json();
console.log(url, provider, path, warning);If you use AWS S3 or Cloudflare R2 with presigned browser uploads, configure bucket CORS so browser uploads to signedUrl can pass preflight.
Required methods:
PUTOPTIONSGETHEAD
Required AllowedHeaders (not ExposeHeaders) should include at least:
content-type— required for presignedPUTrequestscontent-length— typically safelisted, but some providers still require it to be allow-listed
Without these CORS rules, browser uploads can fail even when POST /api/storage/upload-request and POST /api/storage/upload-confirm are correct.
To delete a file, pass the path returned from the upload response.
Endpoint: DELETE /api/storage/file
await fetch('https://api.ub.bitbros.in/api/storage/file', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'sk_live_YOUR_KEY'
},
body: JSON.stringify({
path: 'YOUR_PROJECT_ID/your_file_name.png'
})
});If the path is invalid or the file has already been removed, the API returns 404.
| Limit | Value |
|---|---|
| Maximum file size | 10 MB per file (urBackend-hosted storage only) |
| Total storage per project | 10 MB (Free tier; unlimited with external BYOS) |
Projects connected to external (bring-your-own) storage are not subject to the 10 MB per-file limit.
Ensure `filename`, `contentType`, and `size` are provided for the upload request, and that `filePath` and `size` are sent to the confirm endpoint. The `x-api-key` header is missing or contains an invalid key. Check that you are using a valid key from your project dashboard. The file exceeds the 10 MB per-file limit. Compress or resize the file before uploading. If you use an external S3/R2 bucket, configure bucket CORS to allow `PUT`, `OPTIONS`, `GET`, and `HEAD`, and allow `content-type` and `content-length` headers from your client origin.