Install
$ agentstack add skill-param087-saas-starter-skills-file-uploads-and-storage ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
File Uploads & Storage
Overview
Don't stream uploads through your app server — it's slow, memory-hungry, and hits serverless body limits. Instead, issue a presigned URL so the browser uploads directly to object storage (S3, Cloudflare R2, or Supabase Storage), then store only the object key + metadata in your DB. Validate type/size before signing, namespace keys per tenant, and serve private files via short-lived signed download URLs.
When to use
- Users upload avatars, attachments, imports, or documents.
- Files are large or uploads are slow/timing out through your API.
- You need per-tenant access control on stored files.
Presigned upload
// src/app/api/uploads/sign/route.ts
import { z } from "zod";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { requireOrg } from "@/server/auth/active-org";
const Body = z.object({
filename: z.string().max(200),
contentType: z.enum(["image/png", "image/jpeg", "application/pdf"]), // allowlist
size: z.number().int().positive().max(10 * 1024 * 1024), // 10MB cap
});
const s3 = new S3Client({ region: "auto" });
export async function POST(req: Request) {
const { org } = await requireOrg(new URL(req.url).searchParams.get("org")!);
const { filename, contentType } = Body.parse(await req.json());
const key = `orgs/${org.id}/${crypto.randomUUID()}/${filename}`; // tenant-scoped key
const url = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: "uploads", Key: key, ContentType: contentType }),
{ expiresIn: 60 },
);
return Response.json({ url, key });
}
The client PUTs the file to url, then tells your API the key to persist (in a tenant-scoped row).
Serving files back
- Private by default. Buckets block public reads; generate short-lived signed GET URLs on demand.
- Authorize downloads — verify the requester's org owns the
keybefore signing. - Process async — thumbnails/virus-scan/transcode run in
background-jobs, keyed off the upload.
Pitfalls
- Proxying bytes through the API — memory blowups and serverless payload limits; use presigned URLs.
- Trusting client-reported content-type/size — allowlist types and cap size in the signing request; verify after upload.
- Public buckets for private data — leaks every file via a guessable URL. Keep private, sign reads.
- Un-namespaced keys — without
orgs//..., one tenant can reference another's objects. - Storing the file in Postgres — store the key + metadata; keep bytes in object storage.
Hand-off
Direct-to-storage uploads with tenant-scoped keys. background-jobs post-processes; data-access-layer stores keys; observability-and-errors tracks failures.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: param087
- Source: param087/saas-starter-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.