Customer file uploads for SaaS products
Dreep handles the files your customers upload inside your product: avatars, logos, attachments and documents. Your backend presigns each upload into a folder per tenant, signed URLs keep one tenant's files out of another's reach, OCR reads receipts and invoices, background removal cleans up product shots, and customers who insist on their own bucket get a dedicated BYOB project.
One upload, end to end
The tenant always comes from your session, never from the request body. Dreep validates the file; your backend decides which tenant it belongs to.
User attaches invoice.pdf
Inside your app
Your API authorises
tenantId from session, size and kind checked
Presign
folder: tenants/{tenantId}/documents
Browser PUT, then confirm
Single-use URL, 10 minutes
OCR and signed links
format=txt, expiresIn 300
Folder-per-tenant layout
Access is set on a root folder and inherited by every subfolder that uploads create later. Two or three roots cover a typical B2B product.
| Path | Access | Holds |
|---|---|---|
| brand/{tenantId}/logo | Public | Logos shown on customer portals, invoices and emails |
| people/{tenantId}/{userId} | Public | Avatars, served with width=128&height=128&radius=max&format=webp |
| tenants/{tenantId}/attachments | Signed (root tenants/) | Files on tickets, comments and tasks |
| tenants/{tenantId}/documents | Signed (inherited) | Receipts, invoices, contracts and their OCR text |
| internal/exports | Private | Files only your staff open, inside the Dreep dashboard |
A backend presign endpoint
The whole integration is one authenticated endpoint that presigns, one that confirms, and a helper that signs links when a user opens a file.
import { Dreep } from "dreep";
const dreep = new Dreep({
apiKey: process.env.DREEP_API_KEY,
signingSecret: process.env.DREEP_SIGNING_SECRET,
});
const MAX_BYTES = { attachments: 25_000_000, documents: 50_000_000 };
// POST /api/uploads/presign (your auth middleware sets req.user)
export const postUploadPresign = async (req, res) => {
const { kind, filename, contentType, sizeBytes } = req.body;
const { tenantId } = req.user; // never trust a tenantId sent by the client
if (!MAX_BYTES[kind] || sizeBytes > MAX_BYTES[kind]) {
return res.status(400).json({ error: "Upload not allowed" });
}
const upload = await dreep.presignUpload({
filename,
contentType,
sizeBytes,
folder: "tenants/" + tenantId + "/" + kind,
});
return res.json({ id: upload.id, uploadUrl: upload.uploadUrl });
};
// After loading the file row and checking file.tenantId === req.user.tenantId
export const fileLink = ({ asset }) => dreep.signedUrl(asset, { expiresIn: 300 });
// OCR a stored receipt or PDF in a Signed folder, no re-upload
export const receiptText = async ({ asset }) => {
const res = await fetch(dreep.signedUrl(asset, { expiresIn: 60, transform: { format: "txt" } }));
return res.text();
};var allowedKinds = map[string]bool{"attachments": true, "documents": true}
// POST /api/uploads/presign. tenantFromSession is your own auth helper.
func presignHandler(client *dreep.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tenantID := tenantFromSession(r)
var body struct {
Kind string `json:"kind"`
Filename string `json:"filename"`
ContentType string `json:"contentType"`
SizeBytes int64 `json:"sizeBytes"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || !allowedKinds[body.Kind] {
http.Error(w, "upload not allowed", http.StatusBadRequest)
return
}
upload, err := client.PresignUpload(r.Context(), dreep.PresignOptions{
Filename: body.Filename,
ContentType: body.ContentType,
SizeBytes: body.SizeBytes,
Destination: dreep.Destination{Folder: "tenants/" + tenantID + "/" + body.Kind},
})
if err != nil {
http.Error(w, "presign failed", http.StatusBadGateway)
return
}
json.NewEncoder(w).Encode(upload)
}
}
// After confirm: OCR a receipt in a Signed folder.
text, err := client.ExtractText(ctx, assetID, &dreep.TextOptions{SigningTTL: time.Minute})# Once: Signed root for tenant files
curl -X POST https://api.dreep.cloud/api/v1/folders \
-H "Authorization: Bearer $DREEP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path":"tenants","accessControlType":"signed","defaultExpirySeconds":300}'
# Presign an invoice upload for tenant t_42
curl -X POST https://api.dreep.cloud/api/v1/upload/presign \
-H "Authorization: Bearer $DREEP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"invoice-0193.pdf","contentType":"application/pdf",
"sizeBytes":412337,"folder":"tenants/t_42/documents"}'
# Remove the background from a product shot
curl -X POST https://api.dreep.cloud/api/v1/bg-remove \
-H "Authorization: Bearer $DREEP_API_KEY" \
-F "file=@chair.jpg" -F "folder=catalog/t_42"OCR for receipts and invoices, cutouts for product shots
Expense, accounting and procurement products need the text inside uploads. Request a stored image or PDF with format=txt, or upload and extract in one call with POST /ocr, which can also save the text as a .txt asset next to the file. The JavaScript SDK's extractText returns blocks with bounding boxes when you need positions, for example to find the total on a receipt. OCR is billed per page; extra pages come in packs of 1,000 for $4.99.
Catalogue and storefront builders get product photos shot on desks and kitchen tables. POST /bg-remove returns a new asset with a transparent background, and because the alpha channel is kept, bg=ffffff or any other colour is an ordinary transform afterwards. Every plan includes 25 removals; packs of 1,000 cost $4.99.
Office files, CSV, JSON and ZIP archives are stored as-is and served back unchanged. Images and video go through the full transform set.
When a customer requires their own bucket
Some enterprise customers will only sign if their files sit in a bucket they control. Bring Your Own Bucket supports AWS S3, Cloudflare R2, Wasabi, DigitalOcean Spaces, MinIO and other S3-compatible storage, on Starter or Pro.
BYOB is connected per project and only while the project is empty, and the bucket cannot be changed afterwards. So create a dedicated project for that customer, connect their bucket before the first upload, and store that project's API key against the tenant in your backend. Dreep writes a probe object to test the connection; originals and generated variants then live in the customer's bucket, while delivery still uses Dreep CDN URLs.
Building this yourself versus using Dreep
| Need | Build it yourself | With Dreep |
|---|---|---|
| Direct uploads | Bucket CORS, presign code, a confirm step, cleanup of abandoned uploads | presignUpload, PUT, confirmUpload |
| Tenant isolation | Bucket policies or a proxy that streams every file through your API | Signed folders and locally signed URLs |
| Avatars and logos | A resize worker, a queue, a cache and a CDN in front | URL parameters: width, height, radius, format |
| Receipts and invoices | A separate OCR vendor, contract and billing | format=txt or POST /ocr, billed per page |
| Product cutouts | A model to host or another API to integrate | POST /bg-remove, then bg= on delivery |
What each feature does for a SaaS team
Team seats
Free has 1 seat, Starter 5 and Pro 15, plus 5 more for $3/month. Seats are for your staff in the Dreep dashboard; your customers never need a Dreep account.
Inherited access
A new tenant's folders are created by their first upload and are Signed from the start, because tenants/ is Signed.
Typed SDKs
JavaScript and Go SDKs with typed errors, so a plan limit is a DreepLimitError you can show as a clear message rather than a 500.
Explicit limits
Exceeding a plan limit returns LIMIT_EXCEEDED. No automatic overage billing, so one heavy customer cannot run up your bill.
Who this is for
- B2B products where customers upload avatars, logos, attachments and documents.
- Expense, accounting and procurement tools that need text out of receipts and invoices.
- Teams that must keep tenants' files isolated without proxying every download.
- Vendors with a few enterprise customers who require their own bucket.
When Dreep is not the right fit
- You need malware scanning of customer uploads. Dreep does not scan files; run your own scanner after confirm.
- You need webhooks when an upload finishes. Dreep has none; your confirm call is the event.
- You need in-browser previews of Word or Excel files. Office files are stored as-is, not converted.
Frequently asked questions
How do I keep one tenant from opening another tenant's files?
Put tenant files under a Signed root such as tenants/, read the tenant from the session rather than the request, and only issue a signed URL after checking the file belongs to that tenant. Keep expiries short, because anyone holding a signed URL can open it until it expires.
Do our customers need Dreep accounts?
No. Your backend holds the API key and issues presigned upload URLs and signed download URLs. Dreep team seats are only for your own staff using the dashboard.
Can a customer keep their files in their own S3 bucket?
Yes, with Bring Your Own Bucket on Starter or Pro. Create a dedicated, empty project for that customer and connect their bucket before any upload. The bucket cannot be changed later.
Which file types can customers upload?
Images (JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, HEIF), video (MP4, MOV, WebM) and raw files (PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, CSV, TXT, JSON, ZIP), up to 500 MB each. Raw files are stored as-is.
How is OCR billed?
Per OCR page. Extra pages are sold in packs of 1,000 for $4.99. Reading the text of an asset that has already been extracted is served from cache.
Can the API key live in our frontend?
No. The key is a full-project credential and must stay server-side. The JavaScript SDK refuses to construct in a browser. Browsers upload through presigned URLs your backend creates.
Related
Secure file uploads
What Dreep validates and what your backend still owns.
Bring your own bucket
S3, R2, Wasabi, Spaces and MinIO for customers who need them.
SDKs
The JavaScript and Go SDKs used in the examples above.
Marketplaces
Folder-per-listing uploads and signed verification documents.
Pricing
Seats, OCR pages and background removal add-ons.
Text extraction in the docs
OCR on stored assets and upload-and-extract.
Wire up tenant uploads in an afternoon
Create a Signed tenants folder, add one presign endpoint to your API and open a customer's invoice through a five-minute signed link. Start on the free plan.